Приёмы и хитрости для быстрой front-end разработки
Вступление
Давайте по-честному, настраивая инструмент для сборки проектов (таск-менеджер), большинство разработчиков ищут предыдущий проект с подходящей структурой и просто копируют Grunt или Gulp файл и вносят необходимые правки (меняют названия папок для нового проекта и так далее).
Даже переход с одного таск-менеджера Grunt на другой, более новый, Gulp – это обычно поиск соответсвующего npm-пакета для Gulp, наподобие аналогичного плагина grunt-contrib. Раньше работали со связкой Grunt-contrib-Stylus? Значит, для нового таск-менеджера выбираем Gulp-Stylus. Необходим grunt-contrib-jshint? Наверняка, аналогичный пакет gulp-jshint выполняет те же задачи. А дальше — лишь соответствующие изменения синтактиса.
Таким способом можно быстро и особо не задумываясь настроить таск-менеджер для маленьких проектов. Но после нескольких месяцев работы в Pellucid над приложением, которое в моей практике является самым крупным проектом на Javascript, когда на выполнение задач такс-менеджера всё чаще должно уходить не более 10 секунд, пришлось задуматься об оптимизации.
###Представляем наш демонстрационный проект
В качестве небольшого примера для тестов и экспериментов, я создал демо-репозиторий. В проекте используется jQuery, Lo-dash и Handlebars, а также порядка 50 CommonJS модулей (около 0,5 Mb), чтобы Browserify было, с чем работать.
В этом репозитории наш идеальный таск-менеджер (идеальный вдвойне, потому что очень быстрый — но об этом чуть позже) будет делать следующее:
- предварительная обработка, префиксация и сжатие CSS;
- анализ нашего javascript-кода с подсказками (Lint/Hint), сборка файлов с Browserify и их минификация с Uglify;
- отслеживание изменений и применение к ним задач, указанных выше.
В наших рабочих проектах Pellucid задач при настройке таск-менеджера обычно бывает больше, но эти — самые времязатратные, и в этом примере мы рассмотрим общий случай, применимый ко многим front-end проектам.
Ускорим процессы
В идеальном мире наш Gulpfile мог бы выглядеть так. Довольно просто: мы взяли наш список преобразований и разбили его на Gulp-задачи. Код быстро пишется, легко читается и выполняет всё, что от него требуется.
Однако, даже в нашем сравнительно небольшом демо-приложении, на моём ноутбуке это занимает более 4 секунд (показатель time gulp). Для первоначальной компиляции это допустимое время, но в связи с рекомпиляцией после каждого внесённого изменения благодаря нашей команде watch, время ожидания на тестирование наших изменений становится значительным.
Давайте запустим одни из самых медленных процессов и подумаем, как их ускорить.
Связка Browserify-Watchify
Склеивание модулей с помощью Browserify, пожалуй, наиболее времязатратный процесс в нашей сборке, занимающий около 3 секунд. К счастью, это проще всего исправить. Прежде всего, переключим нашу задачу watch на использование библиотеки Watchify. Она создана тем же человеком, что и Browserify, и в неё встроен механизм, позволяющий пересобирать только необходимые нам файлы. Для Gulp уже есть готовый способ сборки с Watchify, воспользуемся им для начала.
Наш js-task будет по-прежнему использовать Browserify (чтобы компилировать Javascript без запуска команды watch) , но давайте вместо gulp-browserify, который был внесён в чёрный список разработчиками Gulp, воспользуемся Browserify напрямую.
Прим.: gulp-browserify блокирует работу Browserify в версии 3.x. Используя Browserify напрямую, мы можем работать с последней версией 5.x, в которой другие настройки конфигурации.
|
|
А для мониторинга нашей системы файлов мы воспользуемся Watchify, вместо аналогичного Gulp watch.
|
|
Большинство новых параметров, которые мы передаём Browserify, необходимы для работы Watchify. Как вы заметили, мы убрали задачу uglify на этапе watch. Поскольку с помощью этой задачи мы делаем тестовые сборки, не имеет смысла замедлять процесс минификацией.
Теперь, благодаря Watchify, время сборки после пересохранения JS файлов в нашем тестовом проекте уменьшилось с 3-4 секунд до 200 мили-секунд. В Pellucid эти показатели были — с 7 секунд до одной. Одного этого улучшения было бы достаточно, чтобы возгордиться собой и закончить с оптимизацией. Однако, мы можем сделать кое-что ещё.
Дополнительная оптимизация Browserify
Если в своём проекте вы используете библиотеки, для работы которых не нужны сторонние модули, Browserify этого не знает, и будет парсить код на предмет команды require() в любом случае, что часто крайне времязатратно. Чтобы ускорить процесс, вы можете передать Browserify массив модулей, которые не нужно парсить:
Это никак не скажется на времени наших последующих сборок, зато сэкономит нам 200-300мс при первоначальной сборке. Но имейте в виду, если вы используете browserify-shim и сторонний плагин, вы не сможете использовать noparse в зависимостях этого плагина. И, напоследок, если вы используете Browserify в автономной сборке, которая не зависит от контекста CommonJS, работайте с последней версией Browserify, поскольку предыдущие версии использовали derequire, что может сильно замедлить работу вашей сборки. Если вы ограничены версией ранее 5.x., не включайте в задачу watch автономные сборки.
Фильтруем неизменённые файлы
Ещё одна задача, занимающая много времени — анализ кода с подсказками (hinting). Работа jshint в нашем демо-репозитории занимает лишь 500мс, однако, в крупном проекте это время доходит до нескольких секунд.
Для работы с Gulp сборками рекомендуется использовать специальные инструменты, и одним из лучших является gulp-cached. Подключаете в вашу сборку gulp-cached, который кэширует в памяти содержание всех файлов. При последующем выполнении задач, Gulp сначала сравнивает файлы с кэшированными, и подвергает дальнейшим преобразованиям только те файлы, в которые были внесены изменения.
В наших проектах Pellucid эта хитрость экономит несколько секунд в JSHint операциях. Даже в нашем с вами тестовом приложении такой подход уменьшил время на hinting с 500мс до менее 100мс.
|
|
Этот метод будет работать и с другими файлами, которые должны быть обработаны только в случае внесения изменений. Например, если вы сжимаете большое количество изображений, обрабатывая только изменённые фотографии, вы существенно увеличиваете скорость работы.
Если вы беспокоитесь, что это займёт много памяти, особенно в случае с большим количеством изображений, можно установить флаг optimizeMemory, который будет хранить md5 hash вместо полного содержания файлов. Кроме того, можно использовать gulp-change и gulp-newer, которые сравнивают временные метки вместо сохранения содержимого в памяти.
Если понадобится вернуть файлы в поток данных, например, если вы применили hint лишь для изменённых файлов, а связать (concatenate) хотите все имеющиеся файлы — это можно сделать с помощью gulp-remember.
Разделение рабочих задач
Мы не минифицируем (uglify) нашу тестовую JS-сборку, но можно применить кое-какие хитрости и для рабочих сборок. Это несколько увеличит скорость работы, тк мы будем выполнять операции, необходимые лишь для той или иной сборки. Для начала, разрешим передавать флаг –prod в Gulp сборку с помощью gulp-util, и сохраним в переменной для быстрого доступа.
|
|
Теперь внутри различных задачек в зависимости от наличия или отсутствия –prod мы либо выполняем нужные операции, либо применяем gutil.noop() , который просто передаётся в поток.
Например, в нашем Browserify таске, мы создадим карты исходников (source maps) для тестовой сборки, а для рабочей — применим Uglify:
|
|
Аналогично, в нашем Stylus таске, номера строк (или, если хотите, новые подключенные карты исходников) - для тестовой сборки, а минификация — для рабочей:
|
|
Если вам привычнее вместо параметра –prod работать с таском gulp prod, можно делать и так, с помощью run-sequence:
|
|
###Подытожим
Изменения, которые мы внесли в Gulp file, существенно ускорили работу наших сборок , особенно последующих, что крайне важно. Использование Watchify дало наилучший результат, сократив время с 3с до 200мс. Кэширование файлов также сэкономило нам несколько сотен миллисекунд в таске hint. Мы разделили тестовые и рабочие задачи, что позволило совершать только те операции, которые необходимы в данный момент. Это позволит добавлять source maps, отлаживать hints в тестовой сборке, и минифицировать всё — в рабочей.
Для сравнения, вот наш первоначальный Gulpfile, собирающий и пересобирающий всё в течение 4 секунд. А вот наш оптимизированный Gulpfile, с более высокой скоростью сборки и пересборкой в 10 раз быстрее.
###Подождите, а как же… Broccoli?
Broccoli – новый инструмент для сборки проектов, с акцентом на улучшенные пересборки. Если вы внимательно прочитаете пост о первом релизе Broccoli, заметите, что этот таск-менеджер создавался для решения проблем, со многими из которых мы боролись выше.
«Broccoli serve автоматически определяет, какие файлы отслеживать (watch) и пересобирает только те, которые того требуют. Наша цель — менее 200мс на пересборку с типичным стеком сборки».
Звучит отлично, с одной оговоркой. Поддерживаемый сообществом плагин Browserify для Broccoli не работает с Watchify. Способность Broccoli “пересобирать только те файлы, которые того требуют” не применима в контексте полноценной Browserify сборки. Поскольку Browserify с самого начала был нашей основной головной болью, отсутствие Watchify делает Broccoli не привлекательным вариантом на данный момент.
####А как насчёт предварительной обработки CSS?
Признаюсь, об этой теме мы немного умолчали. Рекомендованные Gulp инструменты для последующих сборок, такие как gulp-cached, не сильно нам помогут в тех случаях, когда нужно обрабатывать большое количество входных файлов, объединяя их в один конечный файл. Для последующих CSS сборок понадобится специальный инструмент, делающий примерно то же, что Watchify для Browserify, и подобные инструменты для Stylus мне не известны.
В Pellucid, компиляция более, чем 300 Stylus файлов, содержащих более 2300 селекторов, занимает 2-3 секунды. Изменения в файлы стилей вносятся не так часто, как в javascript файлы, поэтому это допустимые показатели.
Я наслышан ужастиков о компиляции Ruby Sass, занимающей 20-30 и более секунд. В этом случае вам стоит задуматься о переходе на port C в Sass, который в разы быстрее. Вот отличная статья в блоге Treehouse, в которой они рассказали, как сократили время Sass-сборки с 50 до 3 секунд.
####Что ещё можно автоматизировать?
Существует немало способов атвоматизации сборок, что, несомненно, ускоряет рабочий процесс. Существует множество разнообразных способов настраивания вашего Gulp File: от популярных, таких как LiveReload и автоматизированных тестов, до более сложных — поднятие пакетных версий (gulp-bump) и развёртывание на Heroku.
Надеюсь, в этой статье мы затронули самые наболевшие вопросы: типичные задачи, работающие с большим количеством файлов, запускаемые и перезапускаемые при каждом последующем пересохранении файлах. Если вы заметили, что ещё какие-то задачи замедляют вашу работу — и, особенно, если вы придумали, как эту проблему решить — обязательно поделитесь с нами в комментариях!
По мотивам Michael Martin-Smucker