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

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



Есть идея


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

Работа над ним началась совершенно невинно. Команда 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. Если у вашего проекта с этим проблема — данное средство позволит вам об этом узнать.


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


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

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

<!-- Современные браузеры загрузят этот файл: -->
<script type="module" src="/js/app.mjs"></script>
<!-- Старые браузеры загрузят этот файл: -->
<script defer nomodule src="/js/app.js"></script>

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

С другой стороны, вам нужно что-то придумать в том случае, если вас беспокоит воздействие на производительность вашего приложения того факта, что старым браузерам приходится загружать оба бандла. Вот одно потенциальное решение этой проблемы, в котором используется внедрение скриптов (вместо тега <script>, которым мы пользовались выше). Оно позволяет избежать двойной загрузки бандлов соответствующими браузерами. Вот о чём идёт речь:

var scriptEl = document.createElement("script");

if ("noModule" in scriptEl) {
  // Настройка современного скрипта
  scriptEl.src = "/js/app.mjs";
  scriptEl.type = "module";
} else {
  // Настройка устаревшего скрипта
  scriptEl.src = "/js/app.js";
  scriptEl.defer = true; // type="module" откладывается по умолчанию, поэтому установим это здесь.

}

// Внедряем скрипт!
document.body.appendChild(scriptEl);

Этот скрипт предполагает, что если браузер поддерживает атрибут nomodule в элементе script, то он понимает конструкцию type="module". Это обеспечивает то, что устаревшие браузеры будут получать только скрипты, предназначенные для них, а современные — скрипты, предназначенные для них. Однако учитывайте то, что динамически внедрённые скрипты по умолчанию загружаются асинхронно. Поэтому если порядок загрузки зависимостей для вас важен — установите атрибут async в значение false.

Меньше транспилируйте


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


Меньше транспилируйте

А именно — вот о чём идёт речь. Например, параметры по умолчанию — это очень удобная возможность ES6, который вы, возможно, уже пользуетесь:

function logger(message, level = "log") {
  console[level](message);
}

Здесь стоит обратить внимание на параметр level, значением по умолчанию которого является строка log. Это значит, что если мы хотим вызвать console.log с помощью функции-обёртки logger, то нам не нужно передавать этой функции level. Удобно, правда? Всё это хорошо — за исключением того, какой код получается у Babel при трансформации этой функции:

function logger(message) {
  var level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "log";

  console[level](message);
}

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

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

// Исходный код
function logger(...args) {
  const [level, message] = args;

  console[level](message);
}

// Результат работы Babel
function logger() {
  for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
    args[_key] = arguments[_key];
  }

  const level = args[0],
        message = args[1];
  console[level](message);
}

Ситуация усугубляется ещё и тем, что Babel трансформирует этот код даже для проектов с настройками @babel/preset-env, нацеленными на современные браузеры. Это значит, что, даже при использовании дифференциальной загрузки, бандлы, рассчитанные на современные браузеры, будут включать в себя результаты подобной трансформации кода! Для того чтобы смягчить эту проблему — можно использовать режим «слабой» трансформации кода (устанавливая в true параметр loose). Это — идея интересная, так как бандлы, получаемые при включении этого параметра, обычно получаются немного меньшего размера, чем те, которые подвергаются более глубоким преобразованиям. Но использование «слабого» режима трансформации кода может привести к проблемам в том случае, если вы позже уберёте Babel из цепочки инструментов, используемых для сборки проекта.

Вне зависимости от того, пользуетесь ли вы «слабым» режимом преобразования кода, вот один из способов избавиться от неуклюжего транспилированного кода, получаемого при обработке средствами Babel параметров по умолчанию:

// Babel не будет транспилировать этот код
function logger(message, level) {
  console[level || "log"](message);
}

Конечно, параметры по умолчанию — это не единственная современная возможность JavaScript, на которую стоит обращать внимание в коде, который будет подвергнут транспиляции. Обработке подвергается и синтаксис spread, это происходит и со стрелочными функциями, и со многими другими конструкциями.

Если вы не хотите совершенно отказываться от этих возможностей — у вас есть пара способов снизить их негативное влияние на получающийся при транспиляции код:

  1. Если вы создаёте библиотеку — рассмотрите возможность использования @babel/runtime совместно с @babel/plugin-transform-runtime для того, чтобы избавиться от дублирования вспомогательных функций, которые Babel добавляет в ваш код.
  2. Если для реализации неких возможностей приложения нужны полифиллы, то их в код можно включать выборочно. Это делается с помощью пакета @babel/polyfill. Включить его можно, установив параметр babel/preset-env useBuiltins в значение usage.

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

Итоги: улучшение проекта — это не гонка


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

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

Уважаемые читатели! Как вы относитесь к идее отказа от транспиляции JS-кода?

  • +34
  • 10,2k
  • 4
RUVDS.com
919,15
RUVDS – хостинг VDS/VPS серверов
Поделиться публикацией

Комментарии 4

    0
    По моему, все перечисленное применимо к любому языку, даже к тому, где tree shaking входит в состав компилятора.
      0
      В отличие от множества других переводов от RUVDS, вот эту статью нужно отливать в граните. Очень полезный перевод.
        0
        Приятно читать такие комментарии, рады стараться. Мы стараемся находить интересные материалы для перевода, поэтому если вдруг что-то готовое для литья в граните имеется на примете, можете сообщить в лс — переведём для общего блага.
        0
        Понравилась статья, спасибо!

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое