Автор: Ван Нань
Введение: Webpack — это самый популярный интерфейсный инструмент модульного управления ресурсами и упаковки. Как повысить эффективность упаковки стало одним из пунктов, на который все обращают внимание. Далее мы поделимся практическими решениями по оптимизации, которые мы разработали и запустили.
В настоящее время наиболее популярным инструментом для упаковки является Webpack.Что касается схемы оптимизации Webpack, в Интернете есть много статей для ознакомления. Проконсультируйтесь с информацией предшественников, обобщите проблемы, возникшие в проекте, и, наконец, придумайте несколько более практических решений по оптимизации, которыми мы поделимся с вами в этой статье. Поскольку это оптимизация упаковки, нам всегда нужно обращать внимание на следующие моменты:
- Сокращение времени компиляции
- Уменьшить размер выходного файла компиляции
- Улучшить производительность страницы
Подъем области для Webpack 3.0
ScopeHoisting — одна из самых больших новых функций Webpack 3.0. Раньше Webpack объединял каждый модуль (каждый фрагмент кода, который был импортирован или требовался) в отдельную функцию закрытия. Это приведет кbundle
В файле js-файла во внешнем слое каждого модуля будет какая-то специальная упаковка закрытия, что приводит к увеличению размера файла, и в то же время также снижает эффективность выполнения скомпилированного js-файла. в браузере.
Хотя теоретические знания были поняты, мы все еще используем фактическую операцию для проверки:
Включить ScopeHoisting очень просто, потому что в webpack встроена эта функция, а в запись плагина добавлен новый webpack.optimize.ModuleConcatenationPlugin().
plugins:[
new webpack.optimize.ModuleConcatenationPlugin()
]
我们简单的编写两个文件 app.js 和 timer.js。
import timer from './timer'
var time = (new Date()).getTime();
timer();
console.log('hello webpack'+time);
export default function bar(){
console.log('pig');
}
var path = require('path');
var webpack = require('webpack');
var config = {
entry:{
app:'./src/app.js'
},
output:{
filename:'bundle.js',
path: path.resolve(__dirname,'./build')
},
plugins:[
//new webpack.optimize.ModuleConcatenationPlugin()
]
}
module.exports = config;
После компиляции результат такой: размер bundle.js 3.03KB
Мы включаем новый плагин webpack.optimize.ModuleConcatenationPlugin(), и результат компиляции выглядит следующим образом:
Сравнив разницу между ними, вы можете обнаружить, что в bundle.js Webpack2.0 лучше, чемWebpack3.0
Добавлен следующий код:
(function(module, __webpack_exports__, __webpack_require__) {
...
});
И timer.js, и app.js находятся в одной функции, timer.js не компилируется в функции закрытия. Модуль имеет на одну замыкающую функцию меньше, а еще несколько ссылок могут привести к гораздо меньшему количеству. Действительно заметно снижение громкости. В дополнение к этому эффекту эта встроенная оптимизация может значительно повысить эффективность выполнения скомпилированного кода в браузере. В статье Аарона Харди Optimizing Javascript Through Scope Hoisting[1] в реальном проекте Tubine[2] автор сравнивает использованиеScopeHoisting
И не используйте размер пакета и эффективность выполнения js в браузере.Turbine
Размер сжатого файла gzip уменьшается примерно на 41%, а время выполнения инициализации сокращается примерно на 12%. Видеть это, это очень интересно?Если это можно использовать в нашем проекте, это будет прекрасно. К сожалению, реальность жестока, давайте посмотрим на ее ограничения. Я изменил справочный модуль демонстрационного примера наCommonJs
синтаксис, эффект выполнения выглядит следующим образом:
//import bar from './timer'
var bar = require('./timer');
var time = (new Date()).getTime();
bar();
console.log('hello webpack'+time);
// export default function bar(){
// console.log('pig');
// }
exports.bar = function(){
console.log('big');
}
Вы обнаружите, что скомпилированный и упакованный пакет не меняется, когда ScopeHoisting включен с использованием синтаксиса модуля CommonJS. Поскольку в настоящее время webpack3.0 поддерживает только модульный синтаксис ESModule. Подумайте об этом, в вашей собственной структуре большинство зависимостей NPM по-прежнему имеют синтаксис CommonJS.Webpack
Он вернется к исходному режиму упаковки. Когда вы выполняете обработку обновления, вы можете использовать--display-optimization-bailout
Проверьте причину понижения.
В дополнение к синтаксису, который в настоящее время поддерживает ESModlue, возможно, в вашем старом коде есть следующее:
- Используйте ProvidePlugin[3]
- Используется функция eva().
- В проекте несколько записей
Что касается текущей внешней экологической среды, то новые функции ScopeHoisting Webpack 3.0 временно недоступны, ноESModule
Это тенденция, способ ссылки на модули в будущем определенно будет заменен модулем ES.
Использование CommonsChunkPlugin
ScopeHoisting Webpack 3.0 нельзя использовать в реальных проектах, поэтому давайте рассмотрим использование подключаемого модуля CommonsChunkPlugin. Плагин CommonChunkPlugin — это дополнительная функция для создания отдельного файла (он же чанка), который содержит общие модули для чанков с несколькими входами (CommonsChunkPlugin был удален из легато webpack v4, хотите узнать о последней версии). [4].
Идея оптимизации состоит в том, чтобы разделить публичные модули, чтобы итоговый синтезированный файл можно было загрузить один раз в начале, что удобно для последующего доступа к другим страницам, а публичный код в кеше браузера можно было использовать напрямую, что, несомненно, обеспечит лучший опыт. Теоретические знания известны, тогда и попробуем руками.Хороший эффект? Собираем простой на основе Webpack2.7.0Vue
строительные леса:
const path = require('path');
const webpack = require('webpack');
const configw = require('./package.json');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader')
var config = {
entry:{
app:'./src/app.js'
},
output:{
path: path.resolve(__dirname, 'build'),
publicPath: configw.publicPath + '/',
filename: 'js/[name].[chunkhash].js'
},
plugins:[
new CleanWebpackPlugin('build'),
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new ExtractTextPlugin({
filename: 'css/app.css'
}),
+ new webpack.optimize.CommonsChunkPlugin({
+ name:'vender',
+ minChunks: function(module) {
+ return (
+ module.resource &&
+ /\.js$/.test(module.resource) &&
+ module.resource.indexOf(
+ path.join(__dirname, './node_modules')
+ ) === 0
+ )
+ }
+
+ }),
new VueLoaderPlugin(),
],
module:{
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: ['css-loader']
}),
},
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'sass-loader']
})
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
}
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
query: {
presets: ['env']
}
}
]
}
}
module.exports = config;
Для удобства просмотра не введены плагины сжатия для оптимизации. В настоящее время сторонние общедоступные библиотеки, такие как vue, vue-router, axios и т. д. в бизнес-js, которые зависят от node_modules, извлекаются из app.js и упаковываются в vender.js. Чтобы разделить бизнес-js и js-библиотеки сторонних разработчиков, в первую очередь был сделан шаг к тому, чтобы кеш-память браузера не обновлялась часто. К сожалению, если мы изменим бизнес-код, такой как app.js,app.vue
,index.vue
и другой бизнес-код, вы обнаружите, что в дополнение кapp.js
Хэш-значение изменилось, даже те, которые не изменилисьvender.js
Хэши все изменились.
Значение хеша изменилось, и содержимое документа изменилось. Это определенно заставит вас очень торопиться, функция, которую вы хотите создать постоянный кеш JS, в принципе невозможна. Ищите причину, нет файла, зависящего от изменения, почему изменится только бизнес-JS, Vender.js изменится? Поскольку при каждой сборке WebPack генерирует код WebPack Runtime, чтобы помочьWebpack
Выполняйте свою работу, такую как логика загрузки и синтаксического анализа, необходимая для связывания модулей по мере их взаимодействия. Как показано на рисунке ниже, в нашем скаффолдинге сравнение скомпилированных результатов показывает, что отличается только одно значение хеш-функции, сгенерированное во время выполнения.
Поскольку разница настолько мала, давайте извлечем эту часть кода среды выполнения. Это сохранит кэш vender.js. Давай попробуем.
new webpack.optimize.CommonsChunkPlugin({
name:'vender',
minChunks: function(module) {
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, './node_modules')
) === 0
)
}
}),
new webpack.optimize.CommonsChunkPlugin({
name:'manifest',
minChunks:Infinity
})
Код в следующем файле app.vue был изменен, и результаты компиляции до и после выглядят следующим образом:
Хэш vender.js не изменился, как ожидалось. Поскольку код, измененный в файле, был извлечен в файл манифеста. Отношения сопоставления чанков хранятся в манифесте.С сопоставлением чанков мы знаем реальный адрес загружаемого чанка. Тогда каждый раз, когда вы изменяете бизнес-js, вам не нужно развертывать vender.js. Таким образом, третья библиотека зависимостей реализует постоянное кэширование на клиенте и сервере. Меньшие файлы app.js и manifest.js также развертываются с каждым оперативным обновлением. Однако следует отметить, чтоmanifest
должны быть загружены в первую очередь.
Затем используйте эту стратегию развертывания непосредственно в производственной среде, пока нет. Наша цель — реализовать постоянное кэширование сторонних библиотек, но это не может нести риски для нашего запуска. Чтобы убедиться, что вы не выходите в сеть со сторонними библиотеками, пользователи могут напрямую обращаться к сторонним библиотекам в локальном кеше браузера, и даже если бизнес-js обновляется онлайн, он все равно может нормально работать.CommonsChunkPlugin
Этот метод упаковки представляет собой код, компилируемый во время выполнения.Если мы добавляем или удаляем зависимости в бизнес-коде, представьте, что после того, как ваш проект будет запущен таким образом, вы можете оптимизировать проект или добавить функциональные модули.В бизнес-js код зависимая от модуля часть неизбежно изменится. Давайте сравним удержаниеindex.vue
Зависимость в и результат удаления зависимости:
Очевидно, я изменил зависимости модуля в бизнес-коде, что привело к изменениям в библиотеке vender.js, что неизбежно. Поскольку vender.js и app.js тесно связаны, вы можете поместитьruntime
Код извлекается вmanifest
середина. Удаление и добавление импортированных модулей приводит к изменению зависимостей идентификаторов модулей, скомпилированных во время выполнения. Я сравнил дваvender.js
Разница, как показано на рисунке ниже, в основном связана с тем, что идентификатор модуля, на который указывает ссылка, изменился.
Видя это, хотя CommonsChunkPlugin может решить проблему, избежать онлайн-риска невозможно. Не стоит этого делать, чтобы воспользоваться кешированием браузера и, таким образом, выровнять только файл app.js. Трудно гарантировать, что не будет проблем с выходом в интернет. Основная проблема в том, что мы все еще отправляем vender.js после отправки теста после сборки. Конечно, некоторые люди скажут, что если вы не видите значение хеш-функции, вы будете знать, нужно ли вам выходить в интернет или нет.verder.js
. Тем не менее, модификация сценария, о котором я упоминал выше, все еще очень распространена в бизнес-коде, и не стоит снова выходить в интернет.vender.js
. Окончательное решение — использовать DllPlugin, см. ниже.
DllPlugin и DllReferencePlugin
Эти два подключаемых модуля пакета Webpack, Dllplugin, будут упаковывать файл dll и файл карты, на которые ссылается модуль manifest.json. Что поместить в файл dll, зависит от нашей сторонней библиотеки. Это похоже на динамическую библиотеку Windows. Идея использования Dllplugin заключается в том, чтобы упаковать общедоступные сторонние библиотеки в нашем проекте в dll-файл. Он является статическим, если вы вручную не измените библиотеку, которую необходимо импортировать в проект. В то же время также компилируется файл сопоставления manifest.json. Он также является статическим, который сохраняет соответствующую библиотеку js в файле dll через значение сопоставления значений id.DllReferencePlugin
Это должно упаковать сопоставленное значение в наш бизнес-js. Таким образом, сторонняя библиотека зависимостей может быть полностью извлечена заранее. После этого будет запакован и скомпилирован только код бизнес-части, и нет необходимости повторно собирать стороннюю библиотеку js. Время сборки и компиляции значительно сократится.
Докажем это на практике:
Сначала настраиваем конфиг, который генерирует dll.
const path = require("path");
const webpack = require("webpack");
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const config = require('./package.json');
const curDate = new Date();
const curTime = curDate.getFullYear() + '/' + (curDate.getMonth() + 1) + '/' + curDate.getDate() + ' ' + curDate.getHours() + ':' + curDate.getMinutes() + ':' + curDate.getSeconds()
const bannerTxt = config.name + ' ' + config.version + ' ' + curTime;
module.exports = {
//你想要打包的模块数组
entry:{
vendor:['vue','axios','vue-router','qs']
},
output:{
path:path.join(__dirname,'/static/'),
filename:'[name].dll.js',
library:'[name]_library'
//vendor.dll.js 中暴露出的全局变量
//主要是给DllPlugin中的name 使用
//故这里需要和webpack.DllPlugin 中的 'name :[name]_libray 保持一致
},
plugins:[
+ new webpack.DllPlugin({
+ path:path.join(__dirname,'.','[name]-manifest.json'),
+ name:'[name]_library',
+ context:__dirname
+ }),
new UglifyJsPlugin({
cache:true,
sourceMap:false,
parallel:4,
uglifyOptions: {
ecma:8,
warnings:false,
compress:{
drop_console:true,
},
output:{
comments:false,
beautify:false,
}
}
}),
new webpack.BannerPlugin(bannerTxt)
]
}
запись настраивает общую серию корзин семейства Vue. Так как они должны использоваться почти на каждой странице, рекомендуется упомянуть их в общедоступном файле vender.js. Посмотрим на ходовые результаты. Я настроил скрипт npm для выполнения кода npm run dll:
"scripts": {
"dev": "webpack-dev-server -d --open --progress",
"build": "cross-env NODE_ENV=production webpack --hide-modules --progress",
"upload": "cross-env NODE_ENV=upload webpack --hide-modules --progress",
"dll": "webpack --config ./webpack.dll.config.js"
}
Размер сжатого dll.js по-прежнему приемлем. Смотрим на сгенерированныйmanifest.json
Что в нем хранится.
Как и ожидалось, он хранит путь карты ссылок и соответствующее значение идентификатора. dll.js и manifest.json нужно скомпилировать только один раз. После этого нам не нужно снова компилировать и упаковывать, когда мы разрабатываем бизнес-код и упаковываем онлайн.vender.dll.js
. Давайте взглянемwebpack.config.js
как настроить.
const webpack = require('webpack');
const config = require('./package.json');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const autoprefixer = require('autoprefixer');
const htmlwebpackincludeassetsplugin = require('html-webpack-include-assets-plugin');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
const webpackConfig = module.exports = {};
const isProduction = process.env.NODE_ENV === 'production';
const isUpload = process.env.NODE_ENV === 'upload';
const curDate = new Date();
const curTime = curDate.getFullYear() + '/' + (curDate.getMonth() + 1) + '/' + curDate.getDate() + ' ' + curDate.getHours() + ':' + curDate.getMinutes() + ':' + curDate.getSeconds();
const bannerTxt = config.name + ' ' + config.version + ' ' + curTime; //构建出的文件顶部banner(注释)内容
webpackConfig.entry = {
app: './src/app.js',
};
webpackConfig.output = {
path: path.resolve(__dirname, 'build' + '/' + config.version),
publicPath: config.publicPath + '/'+config.version+'/',
filename: 'js/[name].js'
};
webpackConfig.module = {
rules: [{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: ['css-loader', 'postcss-loader']
}),
}, {
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'sass-loader', 'postcss-loader']
})
}, {
test: /\.vue$/,
loader: 'vue-loader',
options: {
extractCSS: true,
postcss: [require('autoprefixer')()]
}
}, {
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
}, {
test: /\.(png|jpg|gif|webp)$/,
loader: 'url-loader',
options: {
limit: 3000,
name: 'img/[name].[ext]',
}
}, ]
};
webpackConfig.plugins = [
new webpack.optimize.ModuleConcatenationPlugin(),
new CleanWebpackPlugin('build'),
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new ExtractTextPlugin({
filename: 'css/app.css'
}),
new CopyWebpackPlugin([
{ from: path.join(__dirname, "./static/"), to: path.join(__dirname, "./build/lib") }
]),
+ new webpack.DllReferencePlugin({
+ context:__dirname,
+ manifest:require('./vendor-manifest.json')
+ })
];
if (isProduction || isUpload) {
webpackConfig.plugins = (webpackConfig.plugins || []).concat([
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new webpack.LoaderOptionsPlugin({
minimize: true
}),
new UglifyJsPlugin({
cache:true,
sourceMap:false,
parallel:4,
uglifyOptions: {
ecma:8,
warnings:false,
compress:{
drop_console:true,
},
output:{
comments:false,
beautify:false,
}
}
}),
new htmlwebpackincludeassetsplugin({
assets:['/lib/vendor.dll.js'],
publicPath:config.publicPath,
append:false
}),
new webpack.BannerPlugin(bannerTxt)
]);
} else {
webpackConfig.output.publicPath = '/';
webpackConfig.devtool = '#cheap-module-eval-source-map';
webpackConfig.plugins = (webpackConfig.plugins || []).concat([
new AddAssetHtmlPlugin({
filepath:require.resolve('./static/vendor.dll.js'),
includeSourcemap:false,
})
]);
webpackConfig.devServer = {
contentBase: path.resolve(__dirname, 'build'),
compress: true, //gzip压缩
historyApiFallback: true,
};
}
Мы используем DllReferencePlugin, чтобы ввести сгенерированный файл сопоставления manifest.json в формальную упаковку бизнес-кода.
app.js весит всего 7,45 КБ, vender.dll.js копируется вbuild
Под содержаниемlib
под папку.
Все бизнес-коды лежат в папке контроля версий, а vender.dll.js помещается в папку lib. Каждый раз, когда вы выходите в интернет, если происходит изменение версии, вам нужно выходить в интернет только с бизнес-js. Нет необходимости запускать папку lib. Пока вы не изменяете вручнуюwebpack.dll.config.js
Вход
entry:{
vendor:['vue','axios','vue-router','qs']
}
никогда не изменится. Или сравните то же самое до и после оптимизации:
До оптимизации app.js занимает 116 КБ из-за запакованной сторонней библиотеки, после оптимизации извлекается третья библиотека.app.js
Всего 7,45 КБ. Просто в начале проекта dll файл нужно запаковать заранее. Каждое последующее время компиляции было почти вдвое меньше, чем раньше. Это просто демонстрация строительных лесов. Эффект более очевиден, когда он используется для создания реального проекта.
Суммировать
Есть много людей, которые не рекомендуют использовать DllPlugin.Они считают, что нет необходимости упаковывать все общедоступные и загружать их на первый экран.Это делает время загрузки первого экрана слишком долгим, и они считают, что еще один config увеличивает нагрузку. Тем не менее, я лично считаю, что для всей серии корзин, таких как React и Vue, общий стек технологий силен. По-прежнему необходимо извлечь все семейство ведра и поместить его в vender.js. Потому что почти каждая страница будет использовать его. Более того, это сторонние библиотеки, которые вообще не имеют ничего общего с бизнес-логикой. Реализация постоянного кэширования для них значительно улучшит работу разработчиков и пользователей. Небольшой опыт строительных лесов, спасибо за просмотр, если у вас есть какие-либо вопросы, добро пожаловать на обсуждение ~
Дальнейшее чтение:
[1]optimizing Javascript Through Scope Hoisting:https://medium.com/launch-by-adobe/optimizing-javascript-through-scope-hoisting-47c132ef27e4
[2]Tubine:https://github.com/Adobe-Marketing-Cloud/reactor-turbine
[3]ProvidePlugin:https://webpack.js.org/plugins/provide-plugin/
[4]SplitChunksPlugin:https://webpack.js.org/plugins/split-chunks-plugin
Статья перенесена из паблика "Исследование полного стека", прошу обратить внимание: