Попытки SЕО оптимизации кода на NextJS

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

Мой вывод — если нужно гнаться за 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. Шрифты подключать через . Это скажется значительно меньше на 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 вместо обычного 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, но не буду. На момент написания статьи не принимал классов и поэтому стилизовать их невозможно было. Обход через обертки очень плох, поскольку это создает 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 
    }
  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, где можно теоритечески управлять загрузкой скриптов

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

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

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

© Habrahabr.ru