Если вы читаете это, вы, вероятно, написали какое-то одностраничное приложение. Вы, наверное, также знаете, что их размер может быстро выйти из-под контроля. В этой статье мы обсудим, как нам удалось увеличить размер нашего основного бизнеса до клиентского приложения, используя несколько методов, включая

  • оптимизация изображения
  • оптимизация зависимостей
  • дерево трясется
  • разделение кода

Приложение

Приложение называется Moje VZP (My VZP), и его основная цель - позволить нашим клиентам получить всю необходимую им информацию о своей медицинской страховке. Это одностраничное приложение React, написанное на TypeScript. Мы используем Webpack для создания пакетов приложений и транспиляции кода с помощью TypeScript и Babel (из-за babel-preset-env, который предоставляет полифилы для старых браузеров). Стили используют SCSS и Bootstrap. Что касается изображений, есть несколько значков SVG и один небольшой баннер PNG.

Старое государство

До того, как мы начали оптимизацию размера, приложение состояло из трех пакетов:

  • vendors.js - все сторонние пакеты (React, lodash и др.)
  • app.js - весь код приложения
  • commons.js - все CSS и изображения

Ниже приведен скриншот вывода webpack-bundle-analyzer (интерактивная версия):

Как мы видим, самые большие части пакета vendors.js - это наши @vzp/* пакеты с их зависимостями - cleave.js (форматированный ввод) и react-intl-tel-input (специальный ввод телефонного номера). Их общий размер составляет 70,84 КБ в сжатом виде. Еще одна большая часть - lodash (32,2 КБ в сжатом виде). Мы нашли способы существенно уменьшить эти размеры (см. Встряхивание дерева и Оптимизация Lodash).

Пакет app.js относительно неинтересен, единственное, что следует отметить, это то, что он содержит код всего приложения в одном пакете. Это означает, что каждый раз, когда пользователь обращается к нашему приложению, он загружает его целиком, даже с частями, которые они не будут (или в некоторых случаях даже не могут) использовать. Мы тоже решили эту проблему (см. Разделение кода).

Наконец, пакет commons.js содержит в основном CSS и изображения. Нам пока не удалось уменьшить размер CSS (он использует Bootstrap по историческим причинам, поэтому нам придется существенно его переписать), однако нам удалось обрезать несколько килобайт изображений (см. Оптимизация изображений ).

Оптимизация изображения

Давайте начнем с самого простого способа уменьшить полезную нагрузку приложения - оптимизации размеров изображений. В Webpack это можно сделать очень легко благодаря замечательному image-webpack-loader. Этот загрузчик использует библиотеку imagemin для оптимизации различных форматов изображений во время сборки Webpack. Настроить это так же просто, как запустить yarn add -D image-webpack-loader и обновить webpack.config.js:

Мы отключаем оптимизацию изображений в режиме разработки, чтобы сборка была максимально быстрой при работе с приложением.

С этим изменением размер изображения в пакете commons.js уменьшился с 7,9 КБ в сжатом виде до 5,85 КБ в сжатом виде. Не очень большой выигрыш в абсолютных цифрах, но, учитывая, что это уменьшение более чем на 25% всего для нескольких строк скрипта сборки, это хорошее сохранение.

Дрожь дерева

Термин встряхивание дерева относится к процессу удаления неиспользуемого кода путем исключения модулей, которые никуда не импортируются. Без тряски дерева, когда вы импортируете часть пакета, включается и весь пакет. Это можно увидеть на нашем @vzp/validatedform:

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

Чтобы воспользоваться преимуществами функции встряхивания дерева Webpack, приложение должно соответствовать нескольким ограничениям как указано в документации:

[1] Используйте синтаксис модуля ES2015 (например, import и export).

[2] Добавьте свойство sideEffects в package.json файл вашего проекта.

[3] Включите минификатор, поддерживающий удаление мертвого кода (например, UglifyJSPlugin).

Вот что мы и сделали. Во-первых, нам пришлось пересмотреть способ построения нашего проекта. Раньше мы использовали TypeScript для переноса нашего кода в ES5 и Babel для обработки полифиллов. Мы изменили это так, что TypeScript выполняет только проверку типов, а Babel обрабатывает транспиляцию и полифилы. Для этого мы обновили наш tsconfig.json:

Как мы видим, TypeScript теперь переносит код в ES6, используя модульную систему ESNext. Остальные три настройки необходимы для работы. Без "moduleResolution": "node" модули разрешаются способом, несовместимым с ESNext, и оба esModuleInterop и allowSyntheticDefaultImports улучшают взаимодействие во время выполнения. Например, они позволяют вам писать import _ from "lodash" там, где раньше вы должны были писать import * as _ from "lodash", что не разрешено в ESNext.

Следующим шагом было обновление .babelrc, установив "modules": false в babel-preset-env опциях:

При этом Babel следует оставить модули в покое, чтобы позволить Webpack получить доступ к импорту и экспорту в той форме, в которой они нужны для встряхивания дерева. В качестве побочного эффекта мы наблюдали уменьшение общего размера пакета просто установкой этого флага (даже в проектах без встряхивания дерева). Фактически существует открытая проблема в репозитории babel-loader, чтобы сделать это значение значением по умолчанию.

Эти два изменения заставляют наше приложение соответствовать первому требованию - используя правильный синтаксис _32 _ / _ 33_. Чтобы пройти второй, нам нужно было обновить наши @vzp/* пакеты.

Обновление было двояким: сначала мы обновили их процесс сборки аналогично тому, что мы описали выше, чтобы они были построены как модули ESNext, затем мы добавили "sideEffects": false в их package.json файлы. Перед этим все пакеты были проверены на отсутствие побочных эффектов во избежание ошибок. Я рад сообщить, что ни одна из наших упаковок не содержит побочных эффектов!

Третье требование было выполнено автоматически, поскольку Webpack 4 по умолчанию использует Uglify в рабочем режиме.

Теперь @vzp/validatedform выглядит намного лучше:

Радио, Флажок и другие неиспользуемые типы ввода не включены, показывая, что встряхивание дерева работает!

Оптимизация Lodash

При включении встряхивания дерева некоторые сторонние зависимости стали меньше и оптимизированы. Однако lodash остался прежним. Изучая причину этого, мы обнаружили, что проблема в том, как мы импортируем функции lodash:

Другими словами, вам нужно импортировать определенные функции из lodash, чтобы встряхивание дерева работало. Вы можете написать импорт вручную, что, однако, быстро становится утомительным и, что более важно, не хватает одного плохого импорта, и вы снова возвращаетесь ко всему lodash, включенному в ваши пакеты.

К счастью, есть способ получше - babel-plugin-lodash. Этот плагин автоматически преобразует неправильный импорт в правильный. Все, что нужно, - это yarn add -D babel-plugin-lodash и одно обновление в .babelrc:

Еще один шаг, предложенный на веб-сайте плагина, - также использовать lodash-webpack-plugin, чтобы сделать lodash еще меньше. Это достигается удалением некоторых более продвинутых (и поэтому редко используемых) функций lodash. Просто настроить: yarn add -D lodash-webpack-plugin и обновить webpack.config.js:

Хотя установка была простой, этот плагин может быть немного сложнее в использовании. Проблемы возникают, когда вы действительно используете в своем коде некоторые «продвинутые» функции. Этот факт может остаться незамеченным - сборка Webpack завершается успешно, пакеты становятся меньше, а затем ваше приложение ведет себя странно или дает сбой. Плагин позволяет выбирать группы функций, устанавливая соответствующие параметры в webpack.config.js, однако бывает сложно определить, какие наборы функций вам нужны. В нашем случае единственный был flattening, потому что мы используем _.flow в нескольких местах. Просто знайте, что использование этого плагина может сломать ваше приложение, если вы не будете осторожны!

Разделение кода

Разделение кода относится к процессу разделения вашего пакета приложений на несколько более мелких, каждый из которых включает часть вашего приложения, и загружает их только тогда, когда они нужны пользователю. Это приводит к более быстрому запуску и экономии полосы пропускания. Кроме того, благодаря HTTP / 2 множественные параллельные запросы не являются проблемой, как раньше.

Основная проблема эффективного разделения кода - это вопрос, где сделать разделение. Самая основная форма разделения кода - это так называемая ориентированная на маршрут. Это происходит из той же логики, что и «обычные» веб-страницы - каждый путь маршрута имеет свой собственный код приложения.

Хотя разделение по маршрутам лучше, чем ничего, существует еще более эффективный подход - разделение по компонентам. Идея в том, что иногда вы хотите быть более детализированными, чем просто маршруты.

Например, в нашем приложении есть целый раздел в разделе «История возмещения», в котором клиенты могут подавать заявки на возмещение расходов на медицинское обслуживание (например, когда они чувствуют, что их поставщик медицинских услуг выставил счет на то, что они не предоставили). Мы называем этот раздел ClaimForm. ClaimForm состоит из многошагового мастера с формами на каждом этапе. Проблема в том, что большинство наших пользователей не используют эту функцию (по крайней мере, не каждый раз, когда они используют наше приложение). Без разделения кода они загружают код ClaimForm, даже если не собираются его использовать.

Чтобы упростить компонентно-ориентированное разделение кода в приложениях React, был создан замечательный пакет react-loadable. Это позволяет разработчикам динамически импортировать части своих приложений красивым декларативным способом. На главной странице проекта есть действительно подробное руководство, поэтому мы не будем вдаваться во все подробности, давайте просто посмотрим, как оно работает!

Как показано в листинге выше, нам нужно только импортировать пакет react-loadable и использовать его для создания нового компонента. Этот компонент создается путем вызова функции Loadable. Loadable принимает объект конфигурации с несколькими опциями. Наиболее важные из них:

  • loader - какой компонент сделать загружаемым
  • loading - какой компонент отображать при загрузке загруженного
  • render - как визуализировать загруженный компонент (необязательно в JavaScript, обязательно в TypeScript)

Первые два варианта говорят сами за себя, хотя render - непростая задача. Хотя в соответствии с документами это необязательно, вы должны указать его, если ваш компонент не является экспортом по умолчанию для своего модуля (например, с использованием export default). Поскольку мы не используем экспорт по умолчанию для компонентов (некоторые из причин упомянуты в замечательном TypeScript Deep Dive book, мы должны указать метод render.

Результирующий компонент затем принимает те же свойства, что и исходный.

Внимательный читатель мог заметить комментарий /* webpackChunkName: "claim-form" */ в операторе динамического импорта. Это используется Webpack для присвоения разделенному чанку имени, удобочитаемого человеком. По умолчанию эти фрагменты просто нумеруются последовательно. Чтобы этот комментарий заработал, необходимо обновить webpack.config.js:

Что касается части сборки, при разделении кода используется синтаксис динамического импорта (этот оператор import()), который Babel не поддерживает по умолчанию. Следовательно, нам нужно установить плагин для этого (yarn add -D babel-plugin-syntax-dynamic-import) и включить его в .babelrc:

И это все, что нужно для настройки! С этого момента Webpack будет генерировать отдельные блоки для наших загружаемых компонентов. Что еще лучше, он также обнаружит код, совместно используемый фрагментами, и также будет выдавать фрагменты с общим кодом.

Результат лучше всего виден на картинке. После разделения по маршрутам (Dashboard, InsuranceHistory и ReimbursementHistory), а также по двум формам (ActivationForm и ClaimForm) и одному элементу UX (PeopleSelector) наши пакеты выглядят следующим образом (интерактивная версия):

Как видите, появилось несколько новых блоков для самих компонентов, а также частей, совместно используемых двумя или более из них. Обратите внимание, что, например, claim-form.js также включает react-intl-tel-input, поскольку это единственный компонент, который его использует. Это означает, что пользователям, которые не используют ClaimForm, больше не нужно загружать этот относительно большой пакет!

Экономия

Главный вопрос: а стоило ли? Ответ (неудивительно) да! Ниже приводится сравнение до и после:

Общая экономия составляет 47,35 КБ в сжатом виде, что примерно на 15% меньше. Что еще более важно, когда пользователи впервые открывают наше приложение (на маршруте Dashboard), они загружают только 159,47 КБ в сжатом виде, что составляет менее половины старой полезной нагрузки!

Вы можете самостоятельно изучить визуализации пакетов - до оптимизации и после оптимизации - чтобы увидеть, что действительно изменилось.

Дальнейшая работа

Обсуждаемые здесь шаги не являются исчерпывающими, все еще есть возможности для улучшения. Самая большая возможность еще больше урезать пакеты - это совместить стили с компонентами (будь то использование CSS-in-JS или «нормальный» импорт) вместо использования одного большого файла CSS. Это позволило бы части Webpack, разделяющей код, обрабатывать стили так же эффективно, как и сценарии.

Резюме

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