В нашей предыдущей статье про голосовых ботов для Рокетбанка хабраюзеры возмутились, что в 2017 году примеры JavaScript для облака Voximplant написаны на ES5. У нас в облаке сильно модифицированный SpiderMonkey, специально обученный не течь и не падать. Тысячи одновременных звонков с параллельно выполняемым JavaScript как бы намекают, что нода – для нас не вариант. Тем не менее, никто не мешает использовать транспайлеры, компилировать ES2017/TypeScript/Elm/Whatever в старый добрый JavaScript и загружать результаты компиляции с помощью Continuous Integration. При таком раскладе возникает соблазн использовать все последние достижения из npmjs, собирая весь код в один ES5 бандл. И вот тут нас ждет засада: даже один метод из lodash дает на выходе бандл размером в полмегабайта. И не похоже, чтобы рекламируемый последние пару лет tree shaking работал.
Кто трясет деревья?
Огромные JavaScript бандлы — это не очень хорошо. В мире браузеров они увеличивают время загрузки страницы: сначала такой бандл надо скачать, потом распарсить, потом выполнить. В мире backend и скриптов-в-облаке тоже свои нюансы. Чтобы выполнять тысячи звонков в секунду, контролируемых через JavaScript, наш SpiderMonkey ограничивает память JavaScript сессии 16-ю мегабайтами. Это на все: исходный код, ast, структуры данных. Архитектура нашей платформы подразумевает, что в облаке выполняется код, который должен работать в реальном времени. А все «тяжелые» вещи можно перенести на свой backend и делать к нему HTTP запросы прямо во время звонка. Беда в том, что пара методов из lodash выглядят как замечательная идея для легковесного кода в облаке. Хоп — и плюс полмегабайта к результирующему JavaScript.
Сообщество JavaScript разработчиков знает об этой проблеме давно, и кроме «давайте выкинем все пробелы и переименуем что не страшно в однобуквенные варианты» (uglify до dead code elimination) активно разрабатывает «Tree Shaking». В идеале, «Tree Shaking» должно убирать весь неиспользуемый код: импорты, вызовы методов, глобальные переменные. И для нашего кода мы должны получить несколько функций lodash, наш код – и всё. А вместо этого получаем lodash целиком. WTF?
Webpack, Rollup и Uglify
Поддержка tree shaking считается сильной стороной rollup, заявлена в последних версиях webpack и уже давно присутствует в UglifyJS в виде «dead code elimination». О разнице между «dead code elimination» два года назад очень хорошо написал автор Rollup: если dead code elimination получает на вход скомпилированный бандл и пытается выкинуть из него неиспользуемый код, то tree shaking работает с AST кода во время компиляции и пытается включить только тот код, который используется. Кстати, Webpack рассчитан на комбинированный подход: вначале tree shaking во время сборки бандла, а затем dead code elimination с помощью UglifyJS плагина.
Только в реальном мире Tree Shaking не работает.
По словам самих авторов, определить используемый код в слабо типизированным языке – задача нетривиальная. И, чтобы ничего не сломать, в непонятных ситуациях код всегда включается. К несчастью, примерами таких «непонятных» ситуаций являются самые популярные библиотеки общего назначения: lodash, underscore, – все вот эти ребята.
Что делать?
Можно, конечно, подождать еще пару лет. Вывод типов становится лучше, ведутся работы над поддержкой tree shaking для типизированных диалектов вроде TypeScript. Но писать ES2017 код с библиотеками хочется сейчас. Без многомегабайтных бандлов.
Сообщество и об этой проблеме знает, поэтому сейчас активно используется временное решение: большие монстры вроде lodash разбиваются на огромную кучу мелких модулей, который можно импортить по отдельности. И тут уже tree shaking сбоев не дает:
Конечно, это демонстрация «в лоб» с пустыми конфигами webpack/rollup. Можно докрутить и до более впечатляющих цифр, но основная идея в том, что не стоит огорчаться тысячам зависимостей, которые ставит yarn. Минимально допиленный напильником стек позволяет выкинуть большую часть неиспользуемого кода и получить вполне читаемый бандл для загрузки в Voximplant или любую другую платформу, которые программируется на JavaScript.
Картинка до ката из статьи про tree shaking в webpack 2
Комментарии (20)
3axap4eHko
05.06.2017 16:34+2но это ведь впоне логично не импортировать целый модуль lodash а отдельный файл(подмодуль) к тому же в es6 большая часть функционала lodash присутствует из коробки вместо ?_.has? можно писать ?[].includes? для массивов или ?a in b? для объектов
kana-desu
05.06.2017 17:10+1Есть плагин для babel, который преобразует
import _ from 'lodash'; import { add } from 'lodash/fp'; const addOne = add(1); _.map([1, 2, 3], addOne);
в
import _add from 'lodash/fp/add'; import _map from 'lodash/map'; const addOne = _add(1); _map([1, 2, 3], addOne);
Вроде как можно настроить не только для lodash, если аналогичные плагины для ramda (бандл заметно уменьшается).
Когда тришейкинг начнет работать, можно будет просто убрать этот плагин, не меняя код.
Jermes
05.06.2017 22:54даже один метод из lodash дает на выходе бандл размером в полмегабайта
Забандленный lodash+axios и пропущенный через UglifyJsPlugin у меня занимает 93.2KB
Nikelandjelo
06.06.2017 04:02+3В тоже время есть Closure Compiler уже как много лет умеет не только трясти деревья но и делать гораздо более сложные оптимизации. Да, требуется использовать типизировать свой код, чтобы достичь наилучшего результата. А если вы уже используете Typescript то стоит посмотрить на https://github.com/angular/tsickle который как раз траспилирует typesecript в js оставляя типы, чтобы потом Closure Compiler всё это заоптимизировал.
some_x
06.06.2017 08:14-3Если у вас такие ограничения по памяти, то зачем вы вообще пишите на javascript?
не проще ли на c++?ACPrikh
06.06.2017 10:44+1Вы, батенька, мешаете играть в любимые игрушки! Если уж говорить о выборе инструмента, соответствующего задаче, то надо вести речь об Elixir (Erlang).
А пока «ежики плакали, кололись, но продолжали жрать кактус».khim
06.06.2017 13:07Проблема в том, что C++-разработчиков мало (про Erlang и говорить не стоит), а JS-разработчиков много. Тяжёлое наследие нулевых. Может лет через 10 всё и изменится, а пока — так.
martynovich
06.06.2017 13:14Тоже такой вопрос возник.
eyeofhell
06.06.2017 13:15Потому что из облака всегда можно сделать HTTP запрос к вашему собственному бэкенду, который ограничен только вашим воображением. Ну и если клиенту понадобится больше — пишите, посмотрим чем можно помочь. Вообще 16 мегабайт для облачного JS сценария в 100 строчек это довольно много.
Druu
06.06.2017 13:16+1> Кстати, Webpack рассчитан на комбинированный подход: вначале tree shaking во время сборки бандла, а затем dead code elimination с помощью UglifyJS плагина.
Вебпак сам по себе не выполняет никакого tree shaking, он только расставляет аннотации, которые потом используются в UglifyJS. И на данный момент это не работает из-за того, что аннотация над IIFE (в которую компилируются классы) не ставится.eyeofhell
06.06.2017 13:18Это точно к последним версиям двойки относится? Потому что https://webpack.js.org/guides/tree-shaking/ — пример оттда компилируется и убирает код без UglifyJS плагина, которого по умолчанию нету. Или я что-то неправильно проверил?
Druu
06.06.2017 15:41+1Действительно, видимо функции обрабатываются отдельно.
Вообще, вот issue:
https://github.com/webpack/webpack/issues/2867
Там много про тришейкинг.eyeofhell
06.06.2017 15:46Все верно, не просто так я эту статью написал. Там все печально :) Пока печально. Но подвижку к улучшению есть!
Druu
06.06.2017 15:50+1> Все верно, не просто так я эту статью написал. Там все печально :) Пока печально. Но подвижку к улучшению есть!
Еще печальнее, когда начинаешь пробовать предложенные там воркэраунды — например, babili у меня собирает проект на ~15клок около получаса и выжирает примерно 8гб памяти (а с дефолтными настройками ноды так и вовсе падает из-за недостатка памяти) :)
Но, да, верим в светлое будущее :)
Druu
06.06.2017 15:47Нет, погодите, там же после обычной компиляции остаются обе функции, только к нужной как раз и добавляется unsused_harmony-аннотация. А потом уже после node_modules/.bin/webpack --optimize-minimize main.js dist.min.js этот кусок удаляется из бандла, но --optimize-minimize как раз и активирует UglifyJs:
https://github.com/webpack/docs/wiki/optimization
Так что все верно.
D1k1y
Вроде facebook flow позволяет делать более гранулированный tree-shaking с помощью flow-графа даже на нетипизированном javascript. https://youtu.be/VEaDsKyDxkY?t=1535
eyeofhell
Это теоретическая возможность или его уже с каким-нить бандлером вроде вебпака успели подружить? По видео товарищь вроде как про «возможности» рассказывает. Но что насчет практики?
3axap4eHko
уже давно используем flow в продакшн вместе с webpack. существует preset для babel который можно использовать со всем, что поддерживает babel