Битва за вершину производительности - преобразование и оптимизация Taro H5

внешний интерфейс Taro
Битва за вершину производительности - преобразование и оптимизация Taro H5

предисловие

Будучи многоцелевой средой разработки, Taro поддерживает компиляцию до конца H5 с момента запуска проекта. Поскольку мультитерминальные возможности Taro продолжают развиваться, наши требования к приложениям Taro H5-terminal также продолжают расти. Нас больше не устраивает «бег», мы надеемся, что Таро умеет быстро бегать.

Мы часто получаем отзывы от пользователей: почему пустые проекты, созданные с помощью скаффолдинга Taro, имеют размер кода 400 КБ+ после упаковки; некоторые пользователи также упоминали в Issue, что некоторые API Taro занимают много места, но на самом деле их функции не выполняются. идеальный и т.п. Как проект с открытым исходным кодом, мы придаем большое значение мнению разработчиков сообщества. Итак, в последней версии мы оптимизировали производительность Taro H5.

В качестве основы среды выполнения каждое приложение Taro H5 должно импортировать основные зависимости, такие как @tarojs/components и @tarojs/taro-h5. После компиляции и упаковки эти зависимости будут занимать более 400 КБ места на первом экране. Если разработчики также используют библиотеки пользовательского интерфейса, такие как Taro-UI, базовый объем будет больше, что сильно ограничивает возможности оптимизации производительности приложений Taro H5.

На самом деле мы не используем все компоненты Taro и API в приложении H5. Ненужно и неразумно упаковывать все эти зависимости в приложение. Устранение мертвого кода для дальнейшего уменьшения размера кода — одно из наших направлений оптимизации.

Эффект

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

Мы создали пустой проект и добавили его в конфигурацию проектаwebpack-bundle-analyzerплагин для просмотра анализа компиляции. На следующем рисунке показан результат анализа файла пакета перед оптимизацией:

before

А после оптимизации контраст очень заметен:

after

Общий размер сгенерированного кода до оптимизации составляет 455 КБ, а после оптимизации осталось всего около 96 КБ, что составляет всего около 1/5 исходного размера.

что вам нужно сделать

Это очень просто, как пользователь, вам не нужно вносить никаких изменений в код, просто обновите Taro до последней версии. Но вне поля зрения Таро большую часть работы делает молча. Далее мы начнем с принципа и подробно познакомим вас с работой Таро.

принцип

Устранение мертвого кода— это метод оптимизации кода, который удаляет код, не влияющий на результат выполнения приложения. В статье Web Fundamentals упоминается, что treeshaking — это форма удаления мёртвого кода, предложенная Rollup.

Tree shaking is a form of dead code elimination. The term was popularized by Rollup, but the concept of dead code elimination has existed for some time.

-- Reduce JavaScript Payloads with Tree Shaking, Jeremy Wagner

По статическому анализу при строительстве инструментов компилятора мы можем проанализировать код в реальных зависимостях. Treeshaking Поставьте наш код, представьте себе дерево, каждая код зависимости рассматривается как узел в дереве. Неиспользованные зависимости удаляются из результатов сборки, что является основной идеей трещины.

Итак, предполагая, что у нас есть фрагмент кода, как мы можем определить его части, которые можно удалить? Ответ, анализируя побочные эффекты:

// add.js
export default function add(a, b){ return a + b; }

// add2.js
console.log('这是一个log')
export default function add2(a, b){ return a + b; }

// index.js
import add from './add.js' // 没有副作用,可以删除
import add2 from './add2.js' // 有副作用,不能直接删除

// EOF

Термин «побочный эффект» определенно знаком студентам, разбирающимся в функциональном программировании. Изменение внешнего состояния или генерация вывода и т. д. — все это побочные эффекты, а код с побочными эффектами нельзя удалить напрямую. Как и в приведенном выше коде, модуль add2 имеет побочные эффекты.

Стоя на плечах гигантов

В дополнение к Rollup существует множество инструментов/плагинов, поддерживающих древовидную структуру, таких как babel-plugin-transform-dead-code-elimination, uglify, terser и т. д. webpack имеет встроенную поддержку treeshaking, начиная с v2, а функция treeshaking была расширена в webpack@4.

Сторона Taro H5 использует веб-пакет в качестве ядра сборки в процессе сборки. Есть несколько моментов, о которых следует помнить при использовании функции treeshaking в webpack:

  1. Если это модуль npm, он обязателенpackage.jsonсуществуют вsideEffectsполя и настроить именно тот исходный код, который имеет побочные эффекты.
  2. Должен использоваться синтаксис модуля ES6. из-за таких вещей, какbabel-preset-envПредварительно настроенный пакет babel, такой как babel, по умолчанию переписывает модульный механизм кода, а также необходимоmodulesУстановить какfalse, который передает работу по синтаксическому анализу модуля непосредственно webpack.
  3. Нужно работать над вебпакомproductionрежим.

Работа webpack по встряске дерева в основном делится на два этапа. Первый шаг — удалить неиспользуемые модули и модули без побочных эффектов на уровне модуля, что делается встроенным плагином webpack; второй шаг — удалить неиспользуемый код на уровне файла, что делается с помощью инструмента минимизации кода Terser. .

Удалить неиспользуемые модули

Как мы упоминали ранее, необходимоpackage.jsonСредняя конфигурацияsideEffectsполе.

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

В webpack анализ зависимостей модулей выполняется встроенными плагинами.SideEffectsFlagPluginнепрерывный.

image-20190225220418363

проходить черезSideEffectsFlagPluginПосле обработки модули, которые не использовались и не имеют побочных эффектов, будут помеченыsideEffectFreeотметка.

существуетModuleConcatenationPluginв, сsideEffectFreeОтмеченные модули не будут упакованы:

image-20190222111301698

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

Удалить неиспользуемый код

В спецификации CommonJS мы передаемrequireфункция импорта модулей черезmodule.exportsэкспортировать. Это означает, что мы можем импортировать и экспортировать модули в любом месте нашего кода любым способом: в функции обратного вызова, которая должна ждать ввода пользователя, или при выполнении определенного условия и т. д. Таким образом, до использования модульной системы ES6 было почти невозможно выполнить анализ зависимостей во время компиляции для Javascript (не совсем невозможно. Prepack может даже выполнять статические вычисления заранее во время компиляции, реализуя интерпретатор JS).

// utils.js
module.exports.add = function (a, b) { return a + b };
module.exports.minus = function (a, b) { return a - b };

// index.js;
var utils = require('./utils.js');

utils.add(1, 2);

Как и код выше, хотя в итоге мы использовали толькоaddфункция, ноminusФункция также появится в окончательном упакованном коде, потому что нет быстрого способа узнать, используется ли она во время компиляции.minusфункция.

В модульной системе ES6 мы используемimport/exportсинтаксис для импорта и экспорта модулей. В отличие от спецификации CommonJS, эта новая модульная система имеет некоторые ограничения:import/exportПоведения могут быть только на верхнем уровне кода, по умолчанию использовать строгий режим и т. д. Эти ограничения делают импорт и экспорт модулей кода статическими, зависимости между модулями определяются во время разработки, и компилятору легче анализировать наш код.

// utils.js
export function add (a, b) { return a + b };
export function minus (a, b) { return a - b };

// index.js;
import { add } from './utils.js';
add(1, 2);

После переоснащения модульной системой ES6 мы ясно видим, что:minusФункция на самом деле не используется, поэтому ее можно безопасно удалить из окончательного упакованного кода.

Конечно, конкретный процесс анализа очень сложен. Продвижение переменных, операция со значением объекта,for(var i in list)Операторы, самовыполняющиеся функции, параметры функций (onClick(function a () {…})) и т. д., может привести к непредвиденным ситуациям, что приведет к сбою встряхивания дерева. Если вы хотите понять конкретный процесс обработки Terser, Baidu/Google будет лучшим учителем.

что Таро сделал

Таро требует некоторых изменений в зависимостях.

Модульность компонентов ES

До модуляризации библиотеки компонентов ES, если должен быть выпущен пакет @tarojs/components, Taro выполнит командуyarn build, используйте webpack для упаковки исходного кода, выводdist/index.jsдокумент. Поскольку webpack не поддерживает экспорт в виде модулей ES, это модуль UMD.

image-20190225154632128

Этот файл занимает 462 КБ, и древовидная сборка невозможна из-за таких проблем, как спецификация модуля. Таким образом, даже если разработчик введет в проект Таро только два компонента, окончательный пакет будет содержать все встроенные компоненты.

На самом деле исходный код @tarojs/components использует спецификацию ESM:

image-20190225160508956

Так что, пока webpack напрямую анализирует исходный код библиотеки компонентов, мы можем сразу же воспользоваться преимуществами собственного treeshaking webpack.

image-20190225162018328

В то же время мы такжеsideEffectsФайл стиля отмечен в атрибуте, чтобы помочь веб-пакету идентифицировать побочные эффекты кода стиля и сохранить код стиля в коде, скомпилированном проектом.

Модульность ES для API

Кроме того, Таро нужно было выполнять команды до того, как был выпущен @tarojs/taro-h5.yarn build, используйте Rollup для упаковки исходного кода, выводdist/index.jsдокумент:

image-20190225162654885

Этот файл занимает 262 КБ. Точно так же, если это приложение Taro H5, сгенерированный код будет полностью импортирован в этот файл.

Мы думали о модульной трансформации @tarojs/taro-h5 и @tarojs/те же компоненты. Мы надеемся, что сам модуль @ tarojs / taro-h5 будет соответствовать спецификациям модуля ESM, его нужно будет только пометить.sideEffects, а затем измените запись модуля.

image-20190225165957461

На первый взгляд, @tarojs/taro-h5 — это что-то вроде «ESM», но этого недостаточно. Нам также нужно экспортировать эти API в виде namedExports, разработчики используютimport { XXX } from '@tarojs/taro-h5'Просто импортируйте API.

image-20190225172609369

Итак, возникает вопрос. В проекте Taro мы использовали defaultImport и не будем использовать API.namedExportsформа:

import Taro from '@tarojs/taro-h5'

Taro.navigateTo()
Taro.getSystemInfo()
// Taro.xxx ...

Пока API передаетсяTaroПеременная Получить атрибут Get,TaroПеременные должны иметь все API, и о дереве не может быть и речи.

Есть ли способ изменить defaultImport на namedImports? Ответ положительный. Мы написали плагин для Babel babel-plugin-transform-taroapi, который заменяет указанные вызовы Api на namedImports, а неуказанные переменные сохраняют форму значений атрибутов. Конкретный исходный код можно посмотреть __здесь__.

// const apis = new Set(['navigateTo', 'navigateBack', ...])
{
  babel: {
    preset: ['babel-preset-env'],
    plugins: [
      // ...,
      ['babel-plugin-transform-taroapi', {
        packageName: '@tarojs/taro-h5',
        apis
      }]
    ]
  }
}

Этот плагин принимает объект в качестве параметра конфигурации:packageNameатрибут указывает имя модуля, который необходимо заменить,apisПринимает объект Set, который представляет собой список всех API.

Чтобы избежать ситуации ручного ведения списка Api позже, мы добавили задачу компиляции в модуль @tarojs/taro-h5 через простой плагин Rollup при выполненииyarn buildСоздайте список API, когда вы командуете:

image-20190225210238592

Ниже приведено сравнение кода до и после компиляции. Как видите, после компиляцииsetStorage,getStorageзаменяются на namedImports.

// 编译前
import Taro from '@tarojs/taro-h5';
Taro.initPxTransform({});
Taro.setStorage()
Taro['getStorage']()

// 编译后
import Taro, { setStorage as _setStorage, getStorage as _getStorage } from '@tarojs/taro-h5';
Taro.initPxTransform({});
_setStorage();
_getStorage();

На данный момент, хотя процесс сложный, наша модульная трансформация @tarojs/taro-h5 наконец-то завершена.

наконец

До настоящего времени Taro имеет высокое завершение конца H5, но это не идеально. В будущем при устранении существующих проблем мы будем продолжать приносить больше новых функций TARO H5, например, довольно высокой в ​​сообществе.switchTab, монитор прокрутки страницыonPageScroll,Потяните вниз, чтобы обновитьonPullDownRefreshДождитесь поддержки Api, более унифицированной анимации переключения страниц, более стабильного многостраничного режима и т.д.

Развитие Таро неотделимо от поддержки сообщества. Большое спасибо разработчикам, которые активно отзываются в группах github и WeChat. Если у вас есть какие-либо мысли или предложения для Таро, Таро приветствует вас, чтобы пожаловаться или посетить:

https://github.com/NervJS/taro