Начинаем с чистой головой

Мой вывод - если нужно гнаться за sео, то NextJS (React) не выбор. Мой предел - 90-93 perfomance по оценке lighthouse. Предложение - NuxtJS (Vue.js).

И вот почему я считаю что это не случайностью:

Рисунок 1 - Perfomance, по оценки Lighthouse, сайтов написанных на Nextjs
Рисунок 1 - Perfomance, по оценки Lighthouse, сайтов написанных на Nextjs
Рисунок 2 - Perfomance, по оценки Lighthouse, сайтов написанных на Nuxtjs
Рисунок 2 - Perfomance, по оценки Lighthouse, сайтов написанных на Nuxtjs

Здесь стоит сделать оговорку о том насколько разные имена здесь и то, что предположительно первым всё равно на оптимизацию, но replit и сам vercel думаю с головой писали их сайты.

Главное что нужно в sео это оптимизация event loop, который я не смог получить на NextJS. Возможно для этого нужно переписать всё приложение, но как-то руки не дошли :) Пока дропаются фреймы, отличного перфоманса не будет.

Рисунок 3 - Запись тасков во вкладке Performance на NextJS
Рисунок 3 - Запись тасков во вкладке Performance на NextJS
Рисунок 4 - Запись тасков во вкладке Perfomance на NuxtJS
Рисунок 4 - Запись тасков во вкладке Perfomance на NuxtJS

Всё же, не всем нужно 100, а на NextJS писать легко и быстро, что также нужно продукту, поэтому ниже к моим наработкам.

Как тестить

Pro tip: если кажется что компонент неоптимизирован, то удалите его и сделайте оценку без него, возможно дело не в нем.

  1. NPM пакет для локального тестирования - lighthouse. Намного стабильнее оценка чем в браузере. Получаете здесь стабильно 100 и вы топ! (буду ждать советов :) )

  2. В браузере проверять в режиме Incognito. Выключает часть сбора всякой инфы и хоть на сайт напрямую это не влияет, но влияет на машину, проводящей оценки производительности.

    1. Если остались включенные расширения с доступом в инкогнито - отключить. Влияют напрямую на загрузку сайта - могут что-то ренденрить, что-то грузить и тд. Адблокеры и гугл переводчики яркий пример.

  3. Хром, даже делая обновление с очисткой кэша, будет что-то кэшировать и всё будет казаться куда оптимизированней. Я остановился в Firefox.

  4. Делая оценку в браузере, после ребилда, обязательно делать новую оценку в новой вкладке, иначе часть будет закешированна.

Шрифты

Самый главный пункт.

  1. Проверяем сколько шрифтов используется и уточняем у дизайнеров нужны ли они. Либо старо, либо уже неактуально, либо легко заменяется, но убирая лишние шрифты вы значительно ускоряете загрузку и упрощаете фикс CLS.

  2. Уменьшаем количество глифов (символов) во всех оставшихся шрифтах с помощью сабсетов шрифтов. Для редактирования шрифтов мне очень был удобен fontsquirrel - делаем сабсеты нужных нам языков (для любого сайта только на английском спокойно хватает latin) и жирностей.

  3. Google Fonts не доверяем и проверяем. В css файлах уже есть ссылки на семейство с разными начертаниями и разными сабсетами (/* latin */ и /* latin-ext */), но очень часто эти ссылки будут одинаковыми, где шрифты "всё в одном". Это не хак и весит он соответственно больше. Такое надо избегать и собирать сабсеты самим.

    1. Быстро посмотреть сколько глифов в шрифте можно, например в консоле. Через код элемента выбираем Elements → справа, где пишутся стили, переключаем вкладку Styles на Computed → снизу написано кол-во глифов в шрифте.

  4. Шрифты подключать через <link rel="preload"/>. Это скажется значительно меньше на TBT чем на CLS.

  5. Шиза-чек: Убрать любые шрифты из CSS в формате base64 (если с этим конечно можно выжить). CSS файлы вырастают до гигантских размеров и начинают грузиться несколько секунд.

Скрипты

  1. Для NextJS с 11-ой версии - использовать только next/script (eslint будет ругаться с использованием расширения plugin:@next/next/recommended). В доке достаточно написано как использовать и какую стратегию выбирать. Вкратце:

    1. Указываем всегда разный id

    2. Стратегии: beforeInteractive - менеджеры сессий, проверки юзера на бота и тд. afterInteractive - менеджеры тэгов и аналитика. lazyOnload - чат плагины и другие медиа виджеты (убедиться что используется не в _document иначе их не будет на сайте, можно добавить в _app)

    Но этот пункт с одним замечанием - статья писалась по опыту оптимизации на NextJS 11.1.2 и тогда с next/script были проблемы. Сравнивая First Load JS при билде, используя next/script вместо обычного </script> First Load выростал на несколько кб.

  2. Для NextJS до 11-ой версии - любые 3rd-party скрипты подключаем с атрибутом defer вместо async

    Рисунок 5 - подключение скриптов с атрибутом async
    Рисунок 5 - подключение скриптов с атрибутом async
    Рисунок 6 - подключение скриптов с атрибутом defer
    Рисунок 6 - подключение скриптов с атрибутом defer
  3. Активно использовать next/dynamic, но только для элементов появляющися по взаимодействую с юзером - бургер меню, дропдауны, попапы и тд. С настройкой { ssr: false } результат выходил лучше всегда.

Images

Здесь хотелось бы упоминуть next/image но не буду. На момент написания статьи <Image> не принимал классов и поэтому стилизовать их невозможно было. Обход через обертки очень плох, поскольку это создает 6 DOM нод (4 div + 1 img + 1 noscript) вместо одной, что не сильно но ухудшает рендер и может встать боком, так как lighthouse требует меньше 250 нод на странице.

  1. Любые картинки, которые не влезают в экран при открытие, использовать с атрибутом loading="lazy". Те что влезают обязательно использовать с width и height. Остальным картинкам они не обязательны, но желательны, так как иногда CLS может зацепить и их.

  2. Если используется много иконок то использовать image sprites - иконки закидывается в одну большую фотографию и используется через background-image и background-position.

    Часто возникающий вопрос - что делать если динамичный размер иконки. Без JS проблему не решить:

    import sprite from 'images/sprite.svg'
    
    const SpriteImage = ({
      spriteCoors,
      defaultHeight,
      defaultWidth,
      height,
      width,
    }) => {
      const widthRatio = width / defaultWidth
      const heightRatio = height / defaultHeight
      const bgWidth = 216 * widthRatio
      const bgHeight = 193 * heightRatio
      const left = -spriteCoors.x * width
      const top = -spriteCoors.y * height
      return <div
        style={{
          position: 'relative',
          width: `${width}px`,
          height: `${height}px`,
          background: `url(${sprite}) ${left}px ${top}px / ${bgWidth}px ${bgHeight}px no-repeat`,
        }}
      />
    }
  3. Все SVG прогонять через SVGOMG. Изредка может не работать и ломать картинку, но на большом количестве иконок сказывается положительно, уменьшая большую часть загрузки.

  4. В какой-то момент нужны будут .webp, поэтому, на момент написания статьи, обязательно использование next-images

    const withImages = require('next-images')
    module.exports = withImages({
      // ... дальше обычный next.config.js
    })

Либы и пакеты

Обязательно проверить каждую либу - возможно она весит 30кб, используете то, что сможете переписать за 5 секунд (как у меня было со Swiper.js)

  1. Оценка веса - bundlephobia. Возможно здесь у меня жиза, но битва за каждый кб. При 10кб уже стоит задуматься. В зависимости от пакета, 1кб может выиграть 1 пункт в Perfomance, а 30кб могут выиграть 20 пунктов.

  2. Обязательно проверьте правильно ли вы используете известные библиотеки. На гитхабе next.js есть куча примеров включая популярные mobx и redux.

Общая оптимизация

  1. Стоит напомнить про requestAnimationFrame. Если каждый фрейм что-то отрисовывается или делается проверка для рендера, то лучше использовать этот его и event loop будет чище.

  2. Обработчики ивентов scroll, resize, touch должны работать через debounce или другой аналог. Никому точно не нужны миллион проверок и функций при каждом пикселе скрола.

  3. Задуматься о апи запросах. Возможно у вас напрашивается batch запрос вместо тысячи единичных запросов.

  4. Еще про апи - использовать шифрование и сжатие. Для koa это koa-compress. Для Nest.js используется compression.

  5. Во всех useEffect возвращать функцию, которая будет очищать таймауты, удалять eventListeners и запрещать изменять стейт в асинхронных функциях. Это просто может выкинуть ошибку, но также может вызывать лишние рендеры

    useEffect(() => {
    	let isMounted = true
    	async f () => {
    		const data = await axios()
    		if (isMounted) { // фиксит ошибку попытки изменить стейт unmounted компонента
    			setState(data)
    		}
    	}
    
    	window.addEventListener('scroll', () => {}) // помним про пункт 2
    
    	return () => {
    		isMounted = false
    
    		clearTimeout(timeout)
    		window.removeEventListener('scroll', () => {})
    	}
    }, [])
  6. Везде используемые next/link всегда используем с prefetch={false}. Не будет грузить вторых и третьих страниц.

То, что осталось за кадром

  • Web Workers. Вещь хорошая, если много логики. Не успел попробовать как сильно спасает и когда логики становится "много".

  • NextJS позволяет создать кастомный next/document, где можно теоритечески управлять загрузкой скриптов

    • Найденные скрипты не отличались перфомансом. Например этот.

    • Под копотом используется чистый нативный реакт, который оптимизированней с создание элементов, что достаточно влияет на перфоманс.

  • Какой-то очень оптимизированный скрипт загрузки стилей. Не успел попробовать.

Комментарии (6)


  1. biomancer
    20.02.2022 17:47

    Про сабсеттинг шрифтов есть один не самый очевидный нюанс - не все лицензии позволяют это делать. Иногда менять файл шрифта запрещено, в том числе для создания сабсета, поэтому стоит проверять условия лицензии перед такими действиями.


  1. borovinskiy
    20.02.2022 23:15
    +2

    По поводу производительности, было дело, наобещал заказчику, что будет 98 баллов, открываем, измеряем и тут оказывается, что оно в районе 73 и в желтой зоне.

    Тут-то я и узнал, что производительность не нормализуется и измеряется в браузере на конкретном железе и если у тебя 98 баллов, это не значит, что у всех 98 баллов.

    Пример сайта цифровой библиотеки на Nuxt.js 2 на двух разных ПК.

    Соответственно, надо не только мерить Lighthouse, но и смотреть на измеренные показатели по пользователям конкретного сервиса.


  1. JamesJGoodwin
    20.02.2022 23:48
    +1

    Мой вывод - если нужно гнаться за sео, то NextJS (React) не выбор

    И вот почему я считаю что это не случайностью

    То есть Вы по одному единственному параметру решили судить о применимости фреймворков в контексте оптимизации производительности для SEO? Какое-то более глубокое сравнение пытались делать? Например, искать корреляцию между количеством скриптов и DOM нод на страницах с использованием разных фреймворков?

    В какой-то момент нужны будут .webp, поэтому, на момент написания статьи, обязательно использование next-images

    Зачем? Достаточно перед загрузкой страницы проверить поддержку avif и webp в браузере через base64 fetch и передать расширение изображения в пропах. Это к Вашему высказыванию:

    битва за каждый кб. При 10кб уже стоит задуматься

    Еще про апи - использовать шифрование и сжатие. Для koa это koa-compress. Для Nest.js используется compression.

    На этом моменте я совершенно запутался. Какое отношение это имеет к SEO и уж тем более какое отношение статья о NextJS имеет к NestJS? В ссылке на документацию к Nest которой вы поделились чёрным по белому написано:

    For high-traffic websites in production, it is strongly recommended to offload compression from the application server - typically in a reverse proxy (e.g., Nginx)

    С учётом того, что свой бэкенд с вероятностью 90% Вы будете хостить на PaaS, у которого есть свой собственный load balancer, он же reverse proxy, - то о сжатии будет беспокоиться именно он и в таком случае сжатие с однопоточного JavaScript движка лучше вообще снять, иначе можете сделать только хуже.

    В конце-концов, готов оспорить самый главный тезис, озвученный Вами в этой статье. NextJS - это один из лучших, если не лучший инструмент для разработки UI с упором на оптимизацию. В подтверждение своих слов демонстрирую Вам скриншот аудита одного из наших NextJS проектов, где на мобильном устройстве оценка достигает 75-77/100 (https://imgur.com/a/20kkHTp), а на десктопе и вовсе 100/100 вышло (https://imgur.com/a/EN7TxOa). Аудит проводили на одной из самых "тяжёлых" страниц с множеством интерактивных компонентов, которая и близко не готова к продакшену - её только пару дней назад закончили "натягивать" с верстки на фреймворк, и ни о какой оптимизации тут и речи не идёт. Тем не менее. Также советую не тратить своё время на попытки добиться 90+ оценки для мобильных устройств, если Вы поставили себе цель гнаться за этим. В этом нет никакого смысла и добиться этого крайне трудно на сайте с интерактивными элементами. В какой-то момент Вы просто заметите, что тратите время на различные "хаки" и анти-паттерн приёмы, которые в дальнейшем будет тяжело поддерживать. Оно того не стоит.

    В заключение предположу, что Вы просто недостаточно хорошо разобрались в теме оптимизации производительности в контексте SEO и решили, что фреймворк плохой и нежели тратить время на детальное изучение решили предложить не использовать его вообще.


  1. andreysam
    22.02.2022 11:09

    Можно попробовать использовать preact вместо react. Не знаю как с SEO, но производительность страниц значительно увеличивается, а размер бандла уменьшается. Жаль не все реакт библиотеки переваривают preact/compat


  1. Vordgi
    22.02.2022 22:36

    Для статей используйте, пожалуйста, универсальный инструмент, который не зависит от вкусов вашей машинки - https://pagespeed.web.dev/
    Сравнил vuestorefront и vercel - в среднем в обоих случаях 72 балла на мобилке, на десктопе у vercel 95 баллов.
    copilot.github.com - ужасный костыль (при всём уважении), грузящий и на десктопе и в мобильной версии картинки png в оригинальном качестве и выполняющий кучу логику на клиенте. В общем-то как и все остальные примеры, по содержимому можно устраивать конкурс на худшую реализацию.
    ---
    Вообще, реальная проблема next - то, что они модерируют примеры не по качеству, а по имени. Увы. (если кому интересно, то речь о странице https://nextjs.org/showcase)
    ---
    К счастью, на конфе с последнего релиза показали две действительно классные (в плане производительности) реализации - Pagespeed insights monogram.io - под 100 баллов на мобильных устройствах и macstadium.com - сейчас ~80, но раньше было также быстро (и по графику реальных пользователей видно, что 99% зеленые).

    monogram.io, за 10 проверок выпадало от 82 до 99 баллов на мобильных устройствах
    monogram.io, за 10 проверок выпадало от 82 до 99 баллов на мобильных устройствах
    macstadium.com среди реальных пользователей
    macstadium.com среди реальных пользователей


  1. Vordgi
    22.02.2022 22:47

    Я конечно понимаю, что web vitals с недавних пор участвуют при приоретизации сайтов в поисковой выдаче, но всё же это только в гугле. Когда я захожу на статью про SEO оптимизацию я ожидаю немного почитать про SEO. Здесь ни о чём кроме производительности речь не идёт.

    Ожидалось про семантику, про то, как метатеги добавляются - где их можно добавить, как они потом мерджатся (при добавлении из разных мест); как работает Link от некста и зачем им своя реализация и почему везде нужно использовать её; почему они рекомендует пре-рендеринг, а не какой-нибудь серверный рендеринг и зачем сделали инкрементальный рендеринг в альтернативу серверному для сайтов с большим количеством страниц.

    В общем хоть про что-нибудь помимо производительности или не брать в название столь обширную тему, думаю про производительность читало бы не меньше людей, чем про SEO