История языка EcmaScript простирается от простого языка сценариев в браузере вплоть до современного языка общего назначения, работающего в различных хост-окружениях. Вместе с усложнением языка появилась и необходимость организации модульной структуры и переиспользования кода с помещением его в библиотеки. Первые библиотеки импортировались за счет загрузки соответствующего JS-файла с хоста поставщика или CDN, а взаимодействие производилось, как правило, посредством экспорта функций и классов с заранее известными именами в глобальное пространство — объект window.
Такая схема применялась достаточно долго, и в простых случаях работает она вполне успешно.
Сложности начинаются, когда библиотек и взаимосвязей между ними становится слишком много.Во-первых, засоряется глобальный хост-объект, и все подключаемые библиотеки должны импортировать не конфликтующие уникальные имена. Во-вторых, нет никакого явного способа обеспечить взаимодействие между библиотеками и осуществить переиспользование.
Вопрос вложенных зависимостей может решаться с помощью
dynamic <script> injection
в DOM-модели, а переиспользование может достигаться за счет экспорта с известным именем в глобальном хост-объекте, однако это не универсальное решение и строится оно исключительно на неявном соглашении между авторами библиотек и использующим их клиентском сценарии. Частично согласование имен решается посредством передачи CDN-серверу в query string
параметров, специфицирующим пространства имен для загружаемой JS-библиотеки, но это также не универсально.Остаются некоторое фундаментальные проблемы, связанные с асинхронной загрузкой и взаимодействием с DOM-моделью. Некоторые библиотеки должны быть загружены раньше других, если вторые имеют зависимость от первых. В случае с динамическим импортом это требует правильной установки async-флага или манипуляции с событием
readystatechanged
, в зависимости от вендора и версии браузера.Конечно, и для этого общего случая есть решение, описанное в статье. Однако, во-первых, оно требует тщательного слежения за зависимостями во всех загружаемых библиотеках, и во-вторых, если некоторые библиотеки представляют собой
polyfills
, которым требуется отслеживание DOM-состояния и событий. В случае defer fallfack
это не заработает.Для универсального решения вышеописанных задач было разработано несколько стандартов организации библиотечных модулей для JS, самые известные из них — AMD (Asyncronous module definition), UMD (Universal module definition) и CommonJS. За счет следования авторами модулей общего формата декларации и наличия общего загрузчика файлов, большинство проблем было решено.
Тем временем активно развивалась платформа Node.JS, где зависимости модулей были решены совершенно другим способом — посредством синхронного require-вызова, а модули имели соответствующий специфичный формат. Тогда технический комитет TC-39 начал разработку универсального средства импорта модулей, которое должно было решать все вышеобозначенные задачи и при этом работать одинаково на сервере и клиенте и обеспечивать синхронную и асинхронную семантику загрузки модуля. Таким средством стали ES6-модули.
Поддержка ES6-модулей посредством transpile и bundle builder
С появлением спецификации Ecmascript 262 version 6 и последующих редакций, в язык добавлялось множество новых синтаксических конструкций и native-функций. Как правило, большинство из них могло легко запускаться и на старых версиях JS-движков за счет предварительного transpile-инга — для синтаксических конструкций, и добавления polyfills — для недостающих функций.
ES6-модули же обеспечивали синхронную не блокирующую семантику загрузки, binding-привязки для экспортируемых/импортируемых сущностей, модульная область видимости и другие аспекты, которые не просто обеспечить обычным transpile-ингом.
Разработчики хотели создавать веб-приложения на актуальном диалекте Ecmascript 6, 7, 8-й и поздних версиях, а для этого требовалось удобство по выполнению transpile-инга и добавлению соответствующих
polyfills
для приложений автоматическим образом, чтобы разработанное приложение могло работать и в относительно старых браузеров без проблем.Совокупным решением этих задач стали
bundle builder
, настраиваемые вместе с подключаемыми transpilers
и polyfills
. Идея состоит в том, что код приложения преобразуется в эталонный диалект, который считается поддерживаемыми всеми актуальными браузерами, например, ES3 или ES5 — в зависимости от задачи. После этого все файлы библиотечных модулей и кода приложения соединяются в один большой файл — так называемый bundle. Этот файл отправляется на клиент и уже не требует никаких синхронных или асинхронных импортов, поскольку весь необходимый код уже находится в bundle
и доступен по кодовым номерам.Известные решения, имплементирующие соответствующий подход: Browserify и Webpack, причем последний в настоящее время является фактически стандартом де-факто. Транспайлером де-факто является Babel. Предложенная схема имеет большое количество преимуществ.
Во-первых, благодаря наличию в схеме transpiler-а, исходный проект может быть фактически написан на любом языке. Как правило, это EcmaScript или TypeScript последней версии, но возможности по расширению синтаксиса практически безграничны. Одно из известных расширений для ES — JSX, используемый в библиотеке React и ее производных.
Во-вторых, за счет контроля преобразования кода в фазе transpile-инга, имеется возможность внедрения поддержки даже такой функциональности, как ES6 proxy или рефлексивной информации в коде.
Среди интересных следствий применения bundle-ирования кода — возможность написание клиентского кода на языке F# или Ocaml и многое другое.
Помимо очевидных преимуществ, решение с bundle-ированием имеет и ряд очевидных недостатков.
Во-первых, результирующий
bundle
, даже с учетом возможного сжатия, имеет довольно большой объем и может быть ощутим при мобильном трафике. Во-вторых, bundle
включает в себя абсолютно все зависимости веб-приложения, которые будут загружены и интерпретированы в браузере пользователя, даже если тот не воспользуется элементами веб-приложения, в которых они нужны. В-третьих, пропадает возможность кэширования библиотечных зависимостей, поскольку bundle
или полностью актуален, или требует полного обновления.Негативные эффекты проявляются и при разработке и отладке приложения. Поскольку bundle-ирование почти всегда сопряжено с
transpile
, то процесс получения нового bundle
, особенно для крупного проекта, может идти долго. Это означает, что в процессе разработки и отладки, после внесения очередного изменения, требуется пересборка bundle
и загрузка его новой версии на клиент. Кроме того, за счет машинного преобразования исходного кода, он становится практически нечитаем, что ведет к сложностям в использовании отладчика в браузере.Конечно, большая часть обозначенных выше проблем имеет свои решения. Для того чтобы в production-режиме не загружать в браузер весь код приложения целиком, в webpack используется технология code chunk splitting. Можно использовать и динамическую версию импорта, возвращающую Promise и обеспечивающую асинхронную загрузку целевого модуля.
Для отладочных целей тоже имеются решения. Просмотр оригинального исходного кода, и даже навигация по нему в отладчике браузера достигается посредством спецификации
source maps
, внедряемых в целевой bundle
в режиме разработки. Частичное обновление без полной перезагрузки bundle
решается при помощи Hot Module Reload, хотя действительно инкрементальное обновление корректно работает только в простых случаях.Нативная поддержка ES6-модулей
Схема с bundle-ированием зависимостей была актуальной для своего времени, но на текущий момент все современные браузеры имеют нативную поддержку ES6-модулей.
Это требует пересмотра взгляда на сборку современных web-приложений, поскольку
bundle
были необходимостью из-за несовершенства и отсутствия требуемой функциональности в браузерах. После её появления, использование нативных конструкций обеспечивает гораздо лучший результат.Во-первых, излишний
transpile
синтаксических конструкций и замены его на эмулирующий код приводит к замедлению и затруднению оптимизаций. Это касается async и generator-функций, заменяемых на regenerator runtime, и лексических переменных let
/const
, преобразуемых в неоптимальные var-декларации.Конечно, это не имеет прямого отношения к ES6-модулям как таковым, но обычно определяется схемой сборки и доставки на клиент приложения. В этом смысле это взаимосвязанные вещи.
Во-вторых, модули эффективны с точки зрения производительности. ES6 модули загружаются и исполняются отложенным образом по умолчанию. Это значит, что невозможно по ошибке осуществить добавление блокирующих модулей к веб-приложению, и соответственно из коробки нет никакой SPOF проблемы.
Для сохранения работоспособности в старых браузерах, не имеющих поддержку ES6-модулей, можно иметь собранный
bundle
и отдавать его для старых агентов. При этом, благодаря особенностям конструкции импорта ES6-модулей, не требуется условной настройки webpack с сегрегацией поставляемого кода в зависимости от User-Agent строки браузера, или средств feature discovery.Для разграничения достаточно следующего кода:
<html>
<head>
<script src="app/index.js" type="module"></script>
<script src="dist/bundle.js" defer nomodule></script>
</head>
<!-- … -->
</html>
Браузер без поддержки ES6-модулей просто загрузит dist/bundle.js и будет работать по старой схеме. Современный браузер возьмет app/index.js в качестве точки входа и будет загружать зависимые ресурсы автоматически.
О вопросах эффективной настройки webpack-а для рассмотренной выше схемы, асинхронной и отложенной загрузки модулей, кэшировании зависимостей, inline-модулях и CORS-политиках для них можно прочесть более детально: «ES6 modules support lands in browsers: is it time to rethink bundling?» и «ECMAScript modules in browsers».
Итоги
Язык EcmaScript прошел большую историю и продолжает развиваться по сей день. Многие решения были актуальны для своего времени и позволяли решать задачи, в том числе упреждающую поддержку функциональности, еще не встроенной в клиентских агентах. Сейчас браузеры и Node.js-сервер выпускает обновление версий достаточно часто, добавляя в них современную функциональность EcmaScript.
В итоге решения, позволяющие в прошлом обеспечивать эмуляцию поддержки новых возможностей в популярных версиях браузеров на сегодня применимы к устаревшим агентам, которые, в зависимости от задачи, имеет смысл поддерживать отдельно или вообще исключить.
Предварительное разрешение и связывание модулей и их последующее bundle-ирование, еще недавно бывшее основным способом поддержки ES6 modules в большинстве браузеров, сейчас оказывает на них негативное влияние и мешает оптимизациям и средствам кэширования.
Таким образом, настраивая сборку веб-приложения, целесообразно предоставлять современным агентам код на современном EcmaScript, включая синтаксические элементы и импорты/экспорты модулей.
От редакции
Курсы «Нетологии» по теме:
- профессия «Frontend-разработчик»;
- профессия «Веб-разработчик»;
- онлайн-программа «Основной курс по JavaScript»;
- онлайн-программа «Node, AngularJS и MongoDB: разработка полноценных веб-приложений»;
- онлайн-программа «JavaScript в браузере: создаем интерактивные веб-страницы».
Комментарии (18)
webschik
21.03.2018 18:36К сожалению не все так гладко с загрузкой bundle для старых браузеров и только модулей для новых. Некоторые из браузеров начинают загружать все подряд — github.com/philipwalton/webpack-esnext-boilerplate/issues/1
kashey
22.03.2018 02:31>Во-первых, излишний transpile синтаксических конструкций и замены его на эмулирующий код приводит к замедлению и затруднению оптимизаций.
Смешались вместе, бандлеры и бабел…
>Предварительное разрешение и связывание модулей и их последующее bundle-ирование, еще недавно бывшее основным способом поддержки ES6 modules в большинстве браузеров, сейчас оказывает на них негативное влияние и мешает оптимизациям и средствам кэширования
Двойку за подготовку. Команда webpack производила исследования после появления HTTP/2, которое показало что HTTP/2 не решает ничего. Пару месяцев назад я перепроверил их результаты — конечно же ничего не изменилось.
Так как узнать зависимости загружаемого файла можно только после его загрузки — загрузка реального приложения разбивается на «волны» дозагрузок, которых можно быть под два десятка без проблем. Берем «обычный» сервер в Германии с пингом 60мс — получили 1с потраченого времени.
В настоящий момент бандер жизненно необходим для практически любого приложения. Это факт с которым поспорить сложно — слишком легко проверяем. Слишком логичен.Synoptic
22.03.2018 10:25+1With HTTP/2 you don’t need to bundle your modules anymore.
Я бы сказал, с HTTP/2 вы больше не захотите бандлить ваши модули. Возможность выкинуть из процесса громоздкий тул — чрезвычайно соблазнительное предложение.
В статье как-то очень скромно про такую важную тему, как кэширование с бандлером и без него:
So we need to find the middle ground to get the best for both worlds. We put the modules into n bundles where n is greater than 1 and smaller than the number of modules. Changing one module invalidates the cache for one bundle which is only a part of the complete application. The remaining application is still cached.
И сколько в итоге делать бандлов? 2, 3, 10? А без бандлера не кусок приложения инвалидируется, а только поменявшийся файл.kashey
22.03.2018 10:31+1Будем честны — с какой вероятностью пользователь зайдет на конкретно ваш сайт через неделю, месяц, год?
Для 99% приложений один бандл — оптимален. Для 1% выделение vendor chunk имеет смысл.
Code splitting? Отдельная песня, и webpack 4 пытается ее решить через авторазбиение, и достаточно успешно.
Но code splitting не зависит от бандлера или «не-бандлера» — он либо есть, либо его нет. Текущие ES6 модули это всегда статическая линковка.Synoptic
22.03.2018 10:37+2Буду честен, я и еще куча народу в других компаниях разрабатывает веб-приложения, которыми пользуются тысячи, и даже миллионы людей. Пользователи продуктов пользуются им регулярно, многие ежедневно. Не все делают сайты, и эта ситуация, скажем так, не редка.
И при частых релизах, а у нормальных продуктов они частые, на регулярной инвалидации кешей ваших мегабандлов вы потеряете намного больше сэкономленного на значительно более редких раундтрипов до сервера(если руками не сбрасывать, то они нужны в начале и при каждой инвадидации кэша, а она тут для файлов, а не для бандлов).kashey
22.03.2018 10:45Если бы люди на самом деле заботились бы о «кеше» — они бы загружали React или, прости господи, moment.js, с какого либо CDN, того же unpkg.
Опять же — большая часть «приложения» сидит в node_modules и при ребилде не меняется.
Vendor chunk чтука хорошая, но как зашарить общие для всех продуктов компании зависимости?
Все понимается в сравнении, у меня есть одно старое приложение которым все еще пользуются миллионы, с оно занимает до gzip меньше меньше чем наше новое приложение после. Раза так в 2. И логики там раз в 10 больше. А кода меньше.
Мистика последние годы с фронтендом творятся. Просто мистика.Synoptic
22.03.2018 10:54Ну так разговор не про тех кому все пофиг:) И не всегда CDN имеет смысл, бывают секьюрные политики, не разрешающие внешние зависимости, плюс CDN при всех его плюсах — это дополнительный DNS fetch, дополнительный TCP-connect. Надо смотреть по ситуации в общем.
Опять же — большая часть «приложения» сидит в node_modules и при ребилде не меняется.
Ну как это не меняется, если вы поправили код клиента, то бандл очевидно поменялся и кеширование слетит. Если не слетело, значит клиентский код не менялся. Если бандл один и в нем лежит все, то он целиком будет перезагружен. Бандл небольшого приложения на Angular в среднем от 600кб и выше.
Мистика последние годы с фронтендом творятся. Просто мистика.
Попробуйте Polymer без бандлера(по слухам, Vue тоже ничего) просто в качестве побаловаться. Мне при всей нестабильности так понравилась эта идея отсутствия сборки, что смотреть на бандлеры теперь тошно.kashey
22.03.2018 10:59+1Я только вчера смотрел в терминал, где крутился старина gulp. 500 ns на ребилд (очень) простого приложения.
Drag13
Попридираюсь
Если вы блокируете основной поток загрузкой скриптов (когда это не нужно, а иногда даже нужно, потому что без них у вас не приложение а белый лист) то кто-то сам себе злобный буратино.
А если серьезно (т.е. почему сейчас это все еще не актуально)
Кроме вышеперечисленного, бандлинг решал проблему ограниченого кол-ва параллельных запросов в браузере. Которая пока что все еще не решена. Поэтому поддержка нативных модулей это хорошо, даже очень, но пока это не массовое решение.
Synoptic
Как я понимаю ограничение на параллельно исполняющиеся запросы было вызвано ограничением на количество TCP-соединений на одного юзера на сервере. С мультиплексированием через HTTP/2 у нас один TCP-коннект для всего, через который гоняется неограниченное число запросов в параллель(кто знает — действительно неограниченное?) и такой проблемы не возникает.
Drag13
И это все и полностью правда. Но пока у нас нет HTTP/2 будем сидеть на бандлах.
barkalov
caniuse.com/#search=http2
Drag13
Да я собтвенно и не против, но где этот дивный новый мир вокруг?
barkalov
Снаружи. w3techs.com/technologies/details/ce-http2/all/all
Drag13
Простите, но
Это не снаружи. Это пока где-то сбоку.
barkalov
Ну не знаю, как по мне это дофига. Это — мейнстрим.
Или вы будете ждать, пока последний цветочный магазин на Битриксе переедет на HTTP/2?