Сейчас, на рубеже десятилетий, самое время критически переоценить то, что считалось правильным в недалёком прошлом, и выяснить, не потеряло ли оно актуальности в наши дни. Иногда вчерашние передовые методики разработки становятся сегодняшними антипаттернами.
Автор статьи, перевод которой мы сегодня публикуем, собирается исследовать три подхода к бандлингу JavaScript-проектов на примере простого Hello World-приложения, созданного с помощью React. Некоторые из приводимых им примеров подразумевают знание читателем основ сборщиков модулей, таких, как Webpack, который, похоже, является сегодня самым популярным среди подобных инструментов.
Главная мысль: не пользуйтесь этим подходом.
При таком подходе сборщик модулей просто используется для упаковки в бандл абсолютно всего — и зависимостей, и кода приложения. На выходе получается нечто вроде большого клубка пряжи. В моём примере сюда входят
До выхода HTTP/2 этот паттерн можно было признать, в некотором роде, вполне приемлемым, так как его использование сокращает количество HTTP-запросов, выполняемых браузером при загрузке материалов страниц. Но учитывая то, что большинство сайтов в наши дни используют HTTP/2, этот паттерн стал антипаттерном.
Почему? Дело в том, что при использовании HTTP/2 выполнение множества запросов больше не создаёт такой же нагрузки на систему, как раньше. В результате упаковка кода в единственный большой бандл больше не даёт проекту существенных преимуществ.
При таком подходе усложняется эффективная организация браузерного кэширования. Например, изменение всего одной строчки кода в простейшем приложении приводит к изменению хэша бандла и к инвалидации кэша, хранящего весь код проекта. В результате всем возвращающимся посетителям приходится снова загружать весь код сайта, даже учитывая то, что этот код на 99% не отличается от того, который они загружали при предыдущем визите. Тут мы имеем дело с нерациональным использованием сетевых ресурсов за счёт многократной передачи от сервера клиенту одних и тех же данных.
HTTP/2 в наши дни поддерживают более 95% клиентов. В 2019 году этот протокол был реализован большинством серверов. Здесь можно найти более подробные сведения об использовании HTTP/2 в 2019 году.
Главная мысль: пожалуйста, используйте этот подход.
Давайте учтём то, о чём мы говорили в предыдущем разделе, и улучшим ситуацию с браузерным кэшированием, отделив код проекта от кода зависимостей. Это решает проблему в вышеописанном случае, когда мы незначительно меняем код проекта и после этого выкладываем обновление в продакшн. Теперь изменился лишь хэш бандла
Если говорить о Webpack, то для реализации этой стратегии понадобятся дополнительные настройки, касающиеся разделения кода. С их помощью мы сообщаем бандлеру о том, где находится код зависимостей. Простой и удобный способ обнаружения такого кода заключается в том, что в пути к соответствующим файлам есть
Вот каким стал код подключения скриптов:
А вот как выглядит водопадный график загрузки страницы, при работе с которой используется HTTP/2.
Водопадный график загрузки страницы
Это — шаг в правильном направлении. Но мы можем продолжить оптимизацию бандлинга. Если обо всём этом поразмыслить — можно понять, что некоторые зависимости проекта изменяются реже других. Возможно, реже всего изменяются
Если продолжить развитие этой мысли, то можно решить, что посетителям сайта, возможно, не нужно загружать весь код проекта для того, чтобы просмотреть всего одну страницу. Зимой некоторые из нас склонны набирать вес — так и файл
Мне подобный уровень разделения кода всё ещё кажется довольно-таки непростым делом. Это пока выглядит как экспериментальная технология (и как нечто такое, в чём вполне могут проявиться трудноуловимые ошибки). Но мне совершенно ясно то, что сильное разделение кода — это то направление, в котором двигается индустрия веб-разработки. Возможно, благодаря поддержке браузерами JavaScript-модулей, мы в итоге сможем полностью отказаться от бандлеров вроде Webpack и просто отдавать клиентам отдельные модули кода. Интересно будет понаблюдать за тем, куда всё это нас приведёт!
Главная мысль: не пользуйтесь этим подходом.
Если вы — из тех, кто немного старомодно относится в веб-разработке (как и я), то у вас может возникнуть внутреннее ощущение того, что мы, возможно, могли бы подключить к странице файл
Отмечу, что при использовании этого подхода надо указать сборщику проектов Webpack на то, что он должен исключить из бандла код
На первый взгляд всё это выглядит вполне здраво, но у такого подхода есть некоторые минусы, которые я предлагаю рассмотреть.
Разработчики старой школы хранят в себе надежду на то, что если все сайты ссылаются на один и тот же CDN-ресурс и используют ту же версию React, что и наш сайт, то посетители нашего сайта, если в кэше их браузеров уже есть код React, не будут тратить время на его повторную загрузку. Это серьёзно повысило бы скорость вывода страниц нашего сайта в рабочий режим. Да и документация React, посвящённая этому вопросу, выглядит многообещающе. Поэтому, наверняка, некоторые разработчики используют этот паттерн. Правильно?
Хотя раньше подобное вполне могло работать, недавно в браузерах, ради повышения безопасности, начали реализовывать механизм разделения кэшей. Речь идёт о том, что даже в идеальных условиях, когда два сайта используют одну и ту же библиотеку, загружаемую по одной и той же CDN-ссылке, код независимо загружается для каждого домена, а кэш, из соображений приватности, попадает в «песочницу», выделенную конкретному домену. Как оказалось, этот механизм уже реализован в Safari (по-видимому, он существует с 2013 года?!). А если говорить о Chrome 77, то для включения разделения кэша пока нужно использовать особый флаг.
Тут делается обоснованное предположение, касающееся того, что использование общедоступных CDN снизится по мере того, как во всё большем количестве браузеров будет реализовано разделение кэша.
Здесь озвучена мысль о том, что использование CDN приводит к повышению нагрузки на системы, так как даже ещё до отправки HTTP-запроса браузеру нужно решить множество задач: DNS-разрешение имени, TCP-соединение, SSL-рукопожатие. Браузеру, для подключению к сайту, в любом случае приходится выполнять эти действия, но если он вынужден подключаться и к CDN — это увеличивает нагрузку на него.
Вот водопадный график, иллюстрирующий процесс загрузки страницы, на которой используются скрипты с общедоступного CDN-ресурса.
Водопадный график загрузки страницы, использующей общедоступные CDN-ресурсы
Красными овалами выделены области, в которых происходят операции, предшествующие выполнению запросов. Кажется, что для простого Hello World-приложения это уже чересчур.
По мере того, как мой простой пример будет развиваться и расти, настанет момент, когда мне захочется использовать в нём собственный шрифт. Например — взятый из Google Fonts. А это означает, что число подобных задержек лишь увеличится, так как для загрузки шрифтов придётся подключаться к соответствующему домену. Тут начинает казаться весьма привлекательной идея хостинга всех ресурсов сайта на собственном основном домене (который, конечно, расположен за собственным CDN-ресурсом проекта, основанным на Cloudflare или Cloudfront).
Если в нашем примере переключиться на загрузку двух React-зависимостей с основного домена сайта — это приведёт к тому, что водопадный график загрузки страницы станет гораздо более аккуратным.
Водопадный график загрузки страницы, не использующей общедоступные CDN-ресурсы
Я, на скорую руку, провёл не особенно научное исследование 32 крупнейших сайтов, использующих React. К своему сожалению, я выяснил, что лишь 10% из них используют общедоступные CDN-ресурсы для загрузки React. Оказалось, правда, учитывая то, какие версии React используют все исследованные сайты, что это особого значения не имеет. В идеальном мире не было бы разделения браузерного кэша, а все сайты могли бы организоваться и использовали бы одни и те же версии скриптов с одних и тех же общедоступных CDN. В реальности же наблюдается чрезвычайно сильный разброс версий React, используемых разными сайтами. Это уничтожает идею общего браузерного кэша.
Версии React, используемые разными сайтами
Если сначала открыть один популярный сайт, использующий React, а потом — другой, то окажется, что шансы того, что оба эти сайта используют одну и ту же версию React, весьма невелики.
Я, в ходе исследования, обнаружил ещё некоторые любопытные сведения об этих React-сайтах. Возможно, они покажутся интересными и вам:
Вот исходные данные моего эксперимента.
К несчастью, в наши дни тот, кто использует общедоступные CDN-ресурсы, сталкивается не только с проблемами, касающимися скорости загрузки страниц, но и с некоторыми другими неприятностями:
Совершенно очевидно то, что будущее лежит за подходом №2.
Размещайте перед своими серверами собственные CDN-ресурсы, использующие HTTP/2 (вроде Cloudflare или Cloudfront). Разбивайте код на небольшие фрагменты для того, чтобы эффективно использовать браузерный кэш. В будущем те фрагменты, на которые разделяют код сайта, могут стать ещё меньше, достигнув размеров отдельных JavaScript-модулей, благодаря тому, что в браузерах начата реализация поддержки данной технологии.
Уважаемые читатели! Пользуетесь ли вы технологиями разделения кода в своих веб-проектах?
Автор статьи, перевод которой мы сегодня публикуем, собирается исследовать три подхода к бандлингу JavaScript-проектов на примере простого Hello World-приложения, созданного с помощью React. Некоторые из приводимых им примеров подразумевают знание читателем основ сборщиков модулей, таких, как Webpack, который, похоже, является сегодня самым популярным среди подобных инструментов.
Подход №1: в бандл попадает абсолютно всё (это похоже на большой клубок ниток)
Главная мысль: не пользуйтесь этим подходом.
При таком подходе сборщик модулей просто используется для упаковки в бандл абсолютно всего — и зависимостей, и кода приложения. На выходе получается нечто вроде большого клубка пряжи. В моём примере сюда входят
react
, react-dom
и код самого приложения. К странице подключается единственный бандл, содержащий весь код проекта:<!-- index.html -->
<script src="bundle.[hash].min.js"></script>
До выхода HTTP/2 этот паттерн можно было признать, в некотором роде, вполне приемлемым, так как его использование сокращает количество HTTP-запросов, выполняемых браузером при загрузке материалов страниц. Но учитывая то, что большинство сайтов в наши дни используют HTTP/2, этот паттерн стал антипаттерном.
Почему? Дело в том, что при использовании HTTP/2 выполнение множества запросов больше не создаёт такой же нагрузки на систему, как раньше. В результате упаковка кода в единственный большой бандл больше не даёт проекту существенных преимуществ.
При таком подходе усложняется эффективная организация браузерного кэширования. Например, изменение всего одной строчки кода в простейшем приложении приводит к изменению хэша бандла и к инвалидации кэша, хранящего весь код проекта. В результате всем возвращающимся посетителям приходится снова загружать весь код сайта, даже учитывая то, что этот код на 99% не отличается от того, который они загружали при предыдущем визите. Тут мы имеем дело с нерациональным использованием сетевых ресурсов за счёт многократной передачи от сервера клиенту одних и тех же данных.
HTTP/2 в наши дни поддерживают более 95% клиентов. В 2019 году этот протокол был реализован большинством серверов. Здесь можно найти более подробные сведения об использовании HTTP/2 в 2019 году.
Подход №2: раздельная упаковка кода проекта и кода сторонних библиотек (разделение кода)
Главная мысль: пожалуйста, используйте этот подход.
Давайте учтём то, о чём мы говорили в предыдущем разделе, и улучшим ситуацию с браузерным кэшированием, отделив код проекта от кода зависимостей. Это решает проблему в вышеописанном случае, когда мы незначительно меняем код проекта и после этого выкладываем обновление в продакшн. Теперь изменился лишь хэш бандла
index
, хранящего собственный код проекта, а хэш бандла vendor
остался неизменным. Посетители, которые возвращаются на обновлённый сайт, загрузят при таком подходе лишь изменённый файл index
, что сэкономит некоторый объём сетевых ресурсов.Если говорить о Webpack, то для реализации этой стратегии понадобятся дополнительные настройки, касающиеся разделения кода. С их помощью мы сообщаем бандлеру о том, где находится код зависимостей. Простой и удобный способ обнаружения такого кода заключается в том, что в пути к соответствующим файлам есть
node_modules
, так как весь код зависимостей хранится в этой папке.Вот каким стал код подключения скриптов:
<!-- index.html -->
<script src="vendor.[hash].min.js"></script>
<script src="index.[hash].min.js"></script>
А вот как выглядит водопадный график загрузки страницы, при работе с которой используется HTTP/2.
Водопадный график загрузки страницы
Это — шаг в правильном направлении. Но мы можем продолжить оптимизацию бандлинга. Если обо всём этом поразмыслить — можно понять, что некоторые зависимости проекта изменяются реже других. Возможно, реже всего изменяются
react
и react-dom
, при этом, когда обновляется одна библиотека, обновляется и другая. В результате можно сделать вывод о том, что эти две библиотеки можно сгруппировать в один логический фрагмент, который можно отделить от других зависимостей, меняющихся чаще, чем react
и react-dom
. Реализовав эту идею, мы получим следующее:<!-- index.html -->
<script src="vendor.react.[hash].min.js"></script>
<script src="vendor.others.[hash].min.js"></script>
<script src="index.[hash].min.js"></script>
Если продолжить развитие этой мысли, то можно решить, что посетителям сайта, возможно, не нужно загружать весь код проекта для того, чтобы просмотреть всего одну страницу. Зимой некоторые из нас склонны набирать вес — так и файл
index
, содержащий код приложения, со временем растёт. В определённый момент может оказаться так, что код проекта оправданно будет подвергнуть дальнейшему разделению, динамически загружая отдельные компоненты и даже организовывая предварительную загрузку отдельных модулей.Мне подобный уровень разделения кода всё ещё кажется довольно-таки непростым делом. Это пока выглядит как экспериментальная технология (и как нечто такое, в чём вполне могут проявиться трудноуловимые ошибки). Но мне совершенно ясно то, что сильное разделение кода — это то направление, в котором двигается индустрия веб-разработки. Возможно, благодаря поддержке браузерами JavaScript-модулей, мы в итоге сможем полностью отказаться от бандлеров вроде Webpack и просто отдавать клиентам отдельные модули кода. Интересно будет понаблюдать за тем, куда всё это нас приведёт!
Подход №3: использование общедоступных CDN для кода некоторых зависимостей
Главная мысль: не пользуйтесь этим подходом.
Если вы — из тех, кто немного старомодно относится в веб-разработке (как и я), то у вас может возникнуть внутреннее ощущение того, что мы, возможно, могли бы подключить к странице файл
vendor.react
, рассмотренный в разделе «Подход №2», воспользовавшись общедоступным CDN-ресурсом:<!-- index.html -->
<script crossorigin src="https://unpkg.com/react@16.12.0/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16.12.0/umd/react-dom.production.min.js"></script>
<script src="index.[hash].min.js"></script>
Отмечу, что при использовании этого подхода надо указать сборщику проектов Webpack на то, что он должен исключить из бандла код
react
и react-dom
.На первый взгляд всё это выглядит вполне здраво, но у такого подхода есть некоторые минусы, которые я предлагаю рассмотреть.
▍Минус №1: использование разными сайтами одних и тех же файлов зависимостей? Уже нет…
Разработчики старой школы хранят в себе надежду на то, что если все сайты ссылаются на один и тот же CDN-ресурс и используют ту же версию React, что и наш сайт, то посетители нашего сайта, если в кэше их браузеров уже есть код React, не будут тратить время на его повторную загрузку. Это серьёзно повысило бы скорость вывода страниц нашего сайта в рабочий режим. Да и документация React, посвящённая этому вопросу, выглядит многообещающе. Поэтому, наверняка, некоторые разработчики используют этот паттерн. Правильно?
Хотя раньше подобное вполне могло работать, недавно в браузерах, ради повышения безопасности, начали реализовывать механизм разделения кэшей. Речь идёт о том, что даже в идеальных условиях, когда два сайта используют одну и ту же библиотеку, загружаемую по одной и той же CDN-ссылке, код независимо загружается для каждого домена, а кэш, из соображений приватности, попадает в «песочницу», выделенную конкретному домену. Как оказалось, этот механизм уже реализован в Safari (по-видимому, он существует с 2013 года?!). А если говорить о Chrome 77, то для включения разделения кэша пока нужно использовать особый флаг.
Тут делается обоснованное предположение, касающееся того, что использование общедоступных CDN снизится по мере того, как во всё большем количестве браузеров будет реализовано разделение кэша.
▍Минус №2: трата ресурсов системы на вспомогательные операции (для каждого домена)
Здесь озвучена мысль о том, что использование CDN приводит к повышению нагрузки на системы, так как даже ещё до отправки HTTP-запроса браузеру нужно решить множество задач: DNS-разрешение имени, TCP-соединение, SSL-рукопожатие. Браузеру, для подключению к сайту, в любом случае приходится выполнять эти действия, но если он вынужден подключаться и к CDN — это увеличивает нагрузку на него.
Вот водопадный график, иллюстрирующий процесс загрузки страницы, на которой используются скрипты с общедоступного CDN-ресурса.
Водопадный график загрузки страницы, использующей общедоступные CDN-ресурсы
Красными овалами выделены области, в которых происходят операции, предшествующие выполнению запросов. Кажется, что для простого Hello World-приложения это уже чересчур.
По мере того, как мой простой пример будет развиваться и расти, настанет момент, когда мне захочется использовать в нём собственный шрифт. Например — взятый из Google Fonts. А это означает, что число подобных задержек лишь увеличится, так как для загрузки шрифтов придётся подключаться к соответствующему домену. Тут начинает казаться весьма привлекательной идея хостинга всех ресурсов сайта на собственном основном домене (который, конечно, расположен за собственным CDN-ресурсом проекта, основанным на Cloudflare или Cloudfront).
Если в нашем примере переключиться на загрузку двух React-зависимостей с основного домена сайта — это приведёт к тому, что водопадный график загрузки страницы станет гораздо более аккуратным.
Водопадный график загрузки страницы, не использующей общедоступные CDN-ресурсы
▍Минус №3: использование разных версий зависимостей различными сайтами
Я, на скорую руку, провёл не особенно научное исследование 32 крупнейших сайтов, использующих React. К своему сожалению, я выяснил, что лишь 10% из них используют общедоступные CDN-ресурсы для загрузки React. Оказалось, правда, учитывая то, какие версии React используют все исследованные сайты, что это особого значения не имеет. В идеальном мире не было бы разделения браузерного кэша, а все сайты могли бы организоваться и использовали бы одни и те же версии скриптов с одних и тех же общедоступных CDN. В реальности же наблюдается чрезвычайно сильный разброс версий React, используемых разными сайтами. Это уничтожает идею общего браузерного кэша.
Версии React, используемые разными сайтами
Если сначала открыть один популярный сайт, использующий React, а потом — другой, то окажется, что шансы того, что оба эти сайта используют одну и ту же версию React, весьма невелики.
Я, в ходе исследования, обнаружил ещё некоторые любопытные сведения об этих React-сайтах. Возможно, они покажутся интересными и вам:
- На 2/3 сайтов для сборки кода используется Webpack.
- 87% сайтов использует HTTP/2, что больше, чем среднее значение в 58%.
- Большинство проектов (примерно 56%) самостоятельно хостит шрифты.
Вот исходные данные моего эксперимента.
▍Минус №4: проблемы, не касающиеся производительности
К несчастью, в наши дни тот, кто использует общедоступные CDN-ресурсы, сталкивается не только с проблемами, касающимися скорости загрузки страниц, но и с некоторыми другими неприятностями:
- Проблемы с безопасностью. Существуют проблемы с безопасностью, связанные с использованием общедоступных CDN-ресурсов, отличные от тех, на решение которых направлено разделение кэша в браузерах. Если, например, хакеры взломают общедоступный CDN-ресурс, они могут очень аккуратно внедрить в библиотеки вредоносный JavaScript-код. У подобного кода будут все привилегии кода сайта, выполняемого на клиенте, он сможет работать с данными пользователей, вошедших на сайт.
- Проблемы с приватностью. Многие компании собирают данные пользователей из запросов, выполняемых сторонними ресурсами. В теории, если на всех сайтах будет реализовано использование общедоступного CDN-ресурса для загрузки кода зависимостей или шрифтов, то этот CDN-ресурс сможет отслеживать сессии пользователей и особенности их работы в интернете. Подобный ресурс сможет делать предположения о том, что их интересует (например, для рекламных целей). И всё это — без использования куки-файлов!
- Единая точка отказа. Распределение материалов, необходимых для функционирования сайта, по множеству доменов, увеличивает шансы того, что, из-за проблем на одном из таких доменов, клиент сможет загрузить лишь часть материалов, нужных для приведения страниц в рабочее состояние. Если говорить честно, то подобные проблемы решаемы средствами JavaScript, но для этого разработчику сайта придётся приложить некоторые дополнительные усилия.
Итоги
Совершенно очевидно то, что будущее лежит за подходом №2.
Размещайте перед своими серверами собственные CDN-ресурсы, использующие HTTP/2 (вроде Cloudflare или Cloudfront). Разбивайте код на небольшие фрагменты для того, чтобы эффективно использовать браузерный кэш. В будущем те фрагменты, на которые разделяют код сайта, могут стать ещё меньше, достигнув размеров отдельных JavaScript-модулей, благодаря тому, что в браузерах начата реализация поддержки данной технологии.
Уважаемые читатели! Пользуетесь ли вы технологиями разделения кода в своих веб-проектах?