Приёмы и хитрости для быстрой front-end разработки

| Категории: Javascript, AngularJS
Eleonora Pavlova

Иллюстрация блокнота

Вступление

Давайте по-честному, настраивая инструмент для сборки проектов (таск-менеджер), большинство разработчиков ищут предыдущий проект с подходящей структурой и просто копируют 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, в которой другие настройки конфигурации.

1
2
3
4
5
6
7
8
9
gulp.task('js', function () {
var browserify = require('browserify'),
source = require('vinyl-source-stream');
return browserify('./js/main.js', {debug: true})
.bundle()
.pipe(source()) // convert to a stream gulp understands
// ... continue with uglify
});

А для мониторинга нашей системы файлов мы воспользуемся Watchify, вместо аналогичного Gulp watch.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
gulp.task('watch', ['stylus'], function () {
var watchify = require('watchify'),
browserify = require('browserify'),
bundler = watchify(browserify('./js/main.js', {
cache: {},
packageCache: {},
fullPaths: true,
transform: ['hbsfy'],
debug: true
}));
function rebundle() {
return bundler.bundle()
.pipe(source('main.js'))
.pipe(gulp.dest('./dist'));
}
bundler.on('update', rebundle);
// run any other gulp.watch tasks
return rebundle();
});

Большинство новых параметров, которые мы передаём Browserify, необходимы для работы Watchify. Как вы заметили, мы убрали задачу uglify на этапе watch. Поскольку с помощью этой задачи мы делаем тестовые сборки, не имеет смысла замедлять процесс минификацией.

Теперь, благодаря Watchify, время сборки после пересохранения JS файлов в нашем тестовом проекте уменьшилось с 3-4 секунд до 200 мили-секунд. В Pellucid эти показатели были — с 7 секунд до одной. Одного этого улучшения было бы достаточно, чтобы возгордиться собой и закончить с оптимизацией. Однако, мы можем сделать кое-что ещё.

Дополнительная оптимизация Browserify

Если в своём проекте вы используете библиотеки, для работы которых не нужны сторонние модули, Browserify этого не знает, и будет парсить код на предмет команды require() в любом случае, что часто крайне времязатратно. Чтобы ускорить процесс, вы можете передать Browserify массив модулей, которые не нужно парсить:

1
2
3
4
browserify('./js/main.js', {
noparse: ['jquery', 'lodash', 'q']
// other options
});

Это никак не скажется на времени наших последующих сборок, зато сэкономит нам 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мс.

1
2
3
4
5
6
7
8
9
10
gulp.task('hint', function () {
var cached = require('gulp-cached'),
jshint = require('gulp-jshint'),
stylish = require('jshint-stylish');
return gulp.src('./js/**/*.js')
.pipe(cached('hinting'))
.pipe(jshint())
.pipe(jshint.reporter(stylish));
});

Этот метод будет работать и с другими файлами, которые должны быть обработаны только в случае внесения изменений. Например, если вы сжимаете большое количество изображений, обрабатывая только изменённые фотографии, вы существенно увеличиваете скорость работы.

Если вы беспокоитесь, что это займёт много памяти, особенно в случае с большим количеством изображений, можно установить флаг optimizeMemory, который будет хранить md5 hash вместо полного содержания файлов. Кроме того, можно использовать gulp-change и gulp-newer, которые сравнивают временные метки вместо сохранения содержимого в памяти.

Если понадобится вернуть файлы в поток данных, например, если вы применили hint лишь для изменённых файлов, а связать (concatenate) хотите все имеющиеся файлы — это можно сделать с помощью gulp-remember.

Разделение рабочих задач

Мы не минифицируем (uglify) нашу тестовую JS-сборку, но можно применить кое-какие хитрости и для рабочих сборок. Это несколько увеличит скорость работы, тк мы будем выполнять операции, необходимые лишь для той или иной сборки. Для начала, разрешим передавать флаг –prod в Gulp сборку с помощью gulp-util, и сохраним в переменной для быстрого доступа.

1
2
3
var gulp = require('gulp'),
gutil = require('gulp-util'),
prod = gutil.env.prod;

Теперь внутри различных задачек в зависимости от наличия или отсутствия –prod мы либо выполняем нужные операции, либо применяем gutil.noop() , который просто передаётся в поток.

Например, в нашем Browserify таске, мы создадим карты исходников (source maps) для тестовой сборки, а для рабочей — применим Uglify:

1
2
3
4
5
6
7
8
9
10
11
12
13
gulp.task('js', function () {
var browserify = require('browserify'),
source = require('vinyl-source-stream'),
streamify = require('gulp-streamify');
return browserify('./js/main.js', {
debug: !prod
})
.bundle()
.pipe(source()) // convert to a stream gulp understands
.pipe(prod ? stream(uglify()) : gutil.noop())
.pipe(gulp.dest('./build'));
});

Аналогично, в нашем Stylus таске, номера строк (или, если хотите, новые подключенные карты исходников) - для тестовой сборки, а минификация — для рабочей:

1
2
3
4
5
6
7
8
9
10
11
gulp.task('stylus', ['cleancss'], function () {
var stylus = require('gulp-stylus'),
prefix = require('gulp-autoprefixer'),
minify = require('gulp-minify-css');
gulp.src('./styl/main.styl')
.pipe(stylus({linenos: !prod}))
.pipe(prefix())
.pipe(prod ? minify() : gutil.noop())
.pipe(gulp.dest('./dist/css'));
});

Если вам привычнее вместо параметра –prod работать с таском gulp prod, можно делать и так, с помощью run-sequence:

1
2
3
4
5
6
7
8
9
10
gulp.task('setProduction', function () {
// global prod variable that we added above
prod = true;
});
gulp.task('prod', function () {
var sequence = require('run-sequence');
// first set our prod flag, then run other tasks
sequence('setProduction', ['stylus', 'js']);
});

###Подытожим

Изменения, которые мы внесли в 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