Хабрахабр

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

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

Есть идея

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

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

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

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

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

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

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

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

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

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

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

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

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

Grunt или gulp — это менеджеры задач. Реализация алгоритма tree shaking — это стандартная возможность современных бандлеров — таких, как webpack, Rollup или Parcel. Менеджер задач, в отличие от бандлера, не создаёт граф зависимостей. Они этим не занимаются. Функционал менеджеров задач можно расширять с помощью плагинов, наделяя их возможностями обрабатывать 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-ресурса. Взглянем на гипотетическую ситуацию, в которой подобное может вашему ресурсу навредить. Однако если вы не укажете в настройках webpack то, что Lodash — это внешняя зависимость, то ваш продакшн код будет загружать библиотеку из CDN, но она, в то же время, будет включена и в состав бандла, который размещён на вашем сервере. Вы, кроме того, установили Lodash в проект для целей локальной разработки.

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

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

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

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

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

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

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

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

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

Велика вероятность того, что в вашей цепочке инструментов присутствует 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 // Внедряем скрипт!
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-кода?

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть