[Перевод] Ответственный подход к JavaScript-разработке, часть 2

В апреле этого года мы опубликовали перевод первого материала из цикла, посвящённого ответственному подходу к JavaScript-разработке. Там автор размышлял о современных веб-технологиях и об их рациональном использовании. Теперь мы предлагаем вам перевод второй статьи из этого цикла. Она посвящена некоторым техническим деталям, касающимся устройства веб-проектов.

fu0jzokbsmkz97fxen8dedmtg9e.jpeg

Есть идея


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

Работа над ним началась совершенно невинно. Команда npm install тут, команда npm install там. Не успели вы оглянуться, как продакшн-зависимости уже устанавливались так, будто разработка проекта — это дикая пьянка, а вы — тот, кого совершенно не заботит то, что будет завтрашним утром.

Потом вы запустились.

Но, в отличие от последствий самой безумной попойки, страшное началось не следующим утром. К сожалению — не следующим утром. Расплата пришла через месяцы. Она приняла неприятную форму лёгкой тошноты и головной боли владельцев компании и менеджеров среднего звена, которые задавались вопросом о том, почему после запуска нового сайта упали конверсии и доходы. Потом бедствие набрало обороты. Случилось это тогда, когда технический директор вернулся с выходных, которые он провёл где-то за городом. Он интересовался тем, почему сайт компании так медленно загружается (если вообще загружается) на его телефоне.

Раньше хорошо было всем. Теперь же настали другие, мрачные времена. Встречайте своё первое похмелье после употребления большой дозы JavaScript.

Это — не ваша вина


В то время как вы пытались справиться с адским похмельем, слова, вроде «Я же тебе говорил», прозвучали бы для вас как заслуженный выговор. А если бы вы способны были бы в то время драться — они могли бы послужить поводом для драки.

Когда дело доходит до последствий необдуманного применения JavaScript — можно осуждать всё и вся. Но искать виновных — это пустая трата времени. Само устройство современного веба требует от компаний решать задачи быстрее, чем их конкуренты. Подобное давление означает, что мы, стремясь как можно сильнее повысить свою продуктивность, скорее всего, ухватимся за всё что угодно. Это означает, что мы, с большой долей вероятности (хотя это и нельзя назвать неизбежным), будем создавать приложения, в которых будет немало излишеств, и, скорее всего, будем использовать паттерны, вредящие производительности и доступности приложений.

Веб-разработка — это непростое занятие. Это — долгая работа. Её редко выполняют хорошо с первой попытки. Самое лучшее в этой работе, однако, это то, что мы не обязаны всё делать идеально в самом её начале. Мы можем вносить в проекты улучшения после их запуска, и, собственно говоря, этому и посвящён данный материал, второй в серии статей об ответственном подходе к JS-разработке. Совершенство — это цель весьма отдалённая. Пока же давайте справимся с JavaScript-похмельем, улучшив, так сказать, скриптуацию
на сайте в ближайшей перспективе.

Разбираемся с распространёнными проблемами


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

▍Примените алгоритм tree shaking


Для начала проверьте — настроены ли используемые вами инструменты на реализацию алгоритма tree shaking. Если вы с этим понятием раньше не сталкивались — взгляните на этот мой материал, написанный в прошлом году. Если пояснить работу этого алгоритма в двух словах, то можно сказать, что благодаря его использованию в состав продакшн-сборок приложения не включают те пакеты, которые, хотя и импортированы в проект, в нём не используются.

Реализация алгоритма tree shaking — это стандартная возможность современных бандлеров — таких, как webpack, Rollup или Parcel. Grunt или gulp — это менеджеры задач. Они этим не занимаются. Менеджер задач, в отличие от бандлера, не создаёт граф зависимостей. Менеджер задач занимается, с использованием необходимых плагинов, выполнением отдельных манипуляций над передаваемыми ему файлами. Функционал менеджеров задач можно расширять с помощью плагинов, наделяя их возможностями обрабатывать JavaScript с использованием бандлеров. Если расширение возможностей менеджера задач в подобном направлении кажется вам проблематичной задачей — то вам, вероятно, нужно вручную проверять кодовую базу и убирать из неё неиспользуемый код.

Для того чтобы алгоритм tree shaking мог бы работать эффективно, необходимо выполнение следующих условий:

  1. Код приложения и установленные пакеты должны быть представлены в виде модулей ES6. Применение алгоритма tree shaking для CommonJS-модулей практически невозможно.
  2. Ваш бандлер не должен трансформировать ES6-модули в модули какого-то другого формата во время сборки проекта. Если это происходит в цепочках инструментов, в которых используется Babel, то в @Babel/present-env должна присутствовать настройка modules: false. Это приведёт к тому, что ES6-код не будет конвертироваться в код, в котором используется CommonJS.


Если вдруг при сборке вашего проекта алгоритм tree shaking не применяется — включение этого механизма может улучшить ситуацию. Конечно, эффективность этого алгоритма варьируется от проекта к проекту. Кроме того, возможность его применения зависит от того, имеют ли импортируемые модули побочные эффекты. Это может повлиять на возможность бандлера избавляться от включения в сборку ненужных импортированных модулей.

▍Разделите код на части


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

  1. Удаляете ли вы дублирующийся код из входных точек?
  2. Выполняете ли вы ленивую загрузку всего, что можно загрузить таким способом, с помощью динамических импортов?


Эти вопросы важны из-за того, что уменьшение объёма избыточного кода — это принципиально значимый элемент производительности. Ленивая загрузка кода также повышает производительность, снижая объём JavaScript-кода, который входит в состав страницы и загружается при её загрузке. Если говорить об анализе проекта на предмет наличия в нём избыточного кода, то для этого можно воспользоваться неким инструментом вроде Bundle Buddy. Если у вашего проекта с этим проблема — данное средство позволит вам об этом узнать.

37fe950f3567d6e53c77969632713df8.png


Средство Bundle Buddy может проверить сведения о webpack-компиляции и выяснить то, как много одинакового кода используется в ваших бандлах

Если же говорить о ленивой загрузке материалов, то тут некоторую сложность может представлять выяснение того, где стоит искать возможности по применению этой оптимизации. Когда я исследую существующий проект на предмет возможности применения ленивой загрузки, я ищу в кодовой базе те места, которые подразумевают взаимодействия пользователя с кодом. Это могут быть, например обработчики событий мыши или клавиатуры, а также прочие подобные вещи. Любой код, для запуска которого необходимы некие действия пользователя, является хорошим кандидатом на применение к нему динамической команды import().

Конечно, загрузка скриптов по запросу несёт в себе риск заметных задержек перехода системы в интерактивный режим. Ведь, прежде чем программа сможет взаимодействовать с пользователем в интерактивном режиме, нужно загрузить соответствующий скрипт. Если объём передаваемых данных вас не беспокоит — рассмотрите возможность применения подсказки по ресурсам rel=prefetch для загрузки подобных скриптов с низким приоритетом. Такие ресурсы не будут соперничать за полосу пропускания с критически важными ресурсами. Если браузер пользователя поддерживает rel=prefetch — использование этой подсказки пойдёт только на пользу. Если нет — ничего страшного не произойдёт, так как браузеры просто игнорируют разметку, которую они не понимают.

▍Используйте опцию webpack externals для пометки ресурсов, расположенных на чужих серверах


В идеале вы должны хостить на собственных серверах как можно больше зависимостей своего сайта. Если же по каким-то причинам вы, без вариантов, должны загружать зависимости с чужих серверов — помещайте их в блок externals в настройках webpack. Если этого не сделать, это может означать, что посетители вашего сайта будут загружать и код, который вы размещаете у себя, и тот же самый код с чужих серверов.

Взглянем на гипотетическую ситуацию, в которой подобное может вашему ресурсу навредить. Предположим, ваш сайт загружает библиотеку Lodash с общедоступного CDN-ресурса. Вы, кроме того, установили Lodash в проект для целей локальной разработки. Однако если вы не укажете в настройках webpack то, что Lodash — это внешняя зависимость, то ваш продакшн код будет загружать библиотеку из CDN, но она, в то же время, будет включена и в состав бандла, который размещён на вашем сервере.

Если вы хорошо знакомы с бандлерами, то всё это может показаться вам прописными истинами. Но я видел, как на эти вещи не обращают внимания. Поэтому не жалейте времени на то, чтобы дважды проверить свой проект на вышеописанные проблемы.

Если вы не считаете нужным самостоятельно хостить свои зависимости, созданные сторонними разработчиками, тогда рассмотрите возможность использования с ними подсказок dns-prefetch, preconnect, или, возможно, даже preload. Это способно снизить показатель сайта TTI (Time To Interactive, время до первой интерактивности). А если для вывода содержимого сайта необходимы возможности JavaScript — то и индекс скорости загрузки (Speed Index) сайта.

Альтернативные библиотеки меньшего размера и снижение дополнительной нагрузки на системы пользователей


То, что называют «Userland JavaScript» (JS-библиотеки, разработанные пользователями), похоже на неприлично огромную кондитерскую. Всё это опенсорсное великолепие и разнообразие внушает нам, разработчикам, священный трепет. Фреймворки и библиотеки позволяют нам расширять наши приложения, быстро оснащая их возможностями, помогающими решать самые разные задачи. Если бы нам пришлось реализовывать тот же функционал самостоятельно — это отнимало бы очень много сил и времени.

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

Возможно это так, а возможно — нет. Это зависит от используемых зависимостей. Например, React — чрезвычайно популярная библиотека. Но Preact — очень маленькая альтернатива React, которая даёт разработчику практически те же API и сохраняет совместимость с множеством дополнений для React. Luxon и date-fns — это альтернативы moment.js, гораздо более компактные, чем эта библиотека, которая не так уж и мала.

В библиотеках вроде Lodash можно найти множество полезных методов. Но некоторые из них легко заменить на стандартные методы ES6. Например, метод Lodash compact, можно заменить на стандартный метод массивов filter. Многие другие методы Lodash тоже можно спокойно заменить на стандартные. Плюс такой замены заключается в том, что мы получаем те же возможности, что и с использованием библиотеки, но избавляемся от довольно крупной зависимости.

Чем бы вы ни пользовались, общая идея остаётся одной и той же: поинтересуйтесь — есть ли у того, что вы выбрали, более компактные альтернативы. Узнайте, можно ли решить те же задачи стандартными средствами языка. Возможно, вы окажетесь приятно удивлены тем, как мало вам придётся приложить усилий для того, чтобы серьёзно сократить размеры приложения и тот объём ненужной нагрузки, которое оно оказывает на системы пользователей.

Пользуйтесь технологиями дифференциальной загрузки скриптов


Велика вероятность того, что в вашей цепочке инструментов присутствует Babel. Это средство применяется для трансформации исходного кода, соответствующего стандарту ES6, в код, который могут выполнять устаревшие браузеры. Означает ли это, что мы обречены отдавать огромные бандлы даже тем браузерам, которые в них не нуждаются, до тех пор, пока все старые браузеры просто не исчезнут? Конечно нет! Дифференциальная загрузка ресурсов помогает обойти эту проблему путём создания на основе ES6-кода двух разных сборок:

  • Первая сборка включает все преобразования кода и полифиллы, необходимые вашему сайту для работы в устаревших браузерах. Вероятно, сейчас вы отдаёте клиентам именно эту сборку.
  • Вторая сборка либо содержит минимум преобразований кода и полифиллов, либо обходится вовсе без них. Она рассчитана на современные браузеры. Это та сборка, которой у вас, возможно, нет. Как минимум — пока нет.


Для того чтобы воспользоваться технологией дифференциальной загрузки сборок, придётся немного поработать. Не буду тут вдаваться в подробности — дам лучше ссылку на мой материал, в котором рассмотрен один из способов реализации этой технологии. Суть этого всего заключается в том, что вы можете модифицировать свою конфигурацию сборки так, чтобы в ходе сборки проекта создавалась бы дополнительная версия JS-бандла вашего сайта. Этот дополнительный бандл будет меньше основного. Он будет предназначен только для современных браузеров. Самое приятное здесь то, что такой подход позволяет добиться оптимизации размеров бандла и при этом не пожертвовать абсолютно ничем из возможностей проекта. В зависимости от кода приложения экономия на размере бандла может оказаться весьма значительной.

58afa780391bbe685486a4a55f99b76b.jpg


Анализ бандла, предназначенного для устаревших браузеров (слева), и бандла, рассчитанного на новые браузеры (справа). Исследование бандлов проведено с помощью webpack-bundle-analyzer. Вот полноразмерная версия этого изображения.

Легче всего отдавать разные бандлы разным браузерам с использованием следующего приёма. Он хорошо работает в современных браузерах:





К несчастью, у этого подхода есть недостатки. Устаревшие браузеры вроде IE11, и даже сравнительно современные — такие, как Edge версий 15–18, загрузят оба бандла. Если вы готовы с этим смириться — тогда пользуйтесь этим приёмом и ни о чём не беспокойтесь.

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