Webpack是当下最热门的前端资源模块强大打包工具。它能够将许多松散的模块按照依赖和规则打包成符合生成环境部署的前端资源;还能够将按需加载的模块进行代码分割,等到实际须要再异步加载。在使用webpack时,若是不注意性能优化,很是大的可能产生性能问题。性能问题主要分为开发时构建速度慢、开发调试时的重复工做、输出打包文件过大等,所以优化啊方案也主要针对这些方面来分析得出。css
webpack打包,首先根据entry配置的入口出发,递归遍历解析所依赖的文件。这个过程分为搜索文件和匹配文件进行分析、转化的2个过程,所以能够从这两个角度来进行优化配置。html
减少文件搜索的优化配置以下:前端
modules: [path.resolve(__dirname, "src"), "node_modules"]
复制代码
设置resolve.mainFields:['main'],设置尽可能少的值,能够减小入口文件的搜索解析node
webpack打包Node应用程序默认会从module开始解析,resolve.mainFields默认值为:react
mainFields: ["module", "main"]
复制代码
第三方模块为了适应不一样的使用环境,会定义多个入口文件。咱们能够设置mainFields统一第三方模块的入口文件main,减小搜索解析。(大多数第三方模块都使用main字段描述入口文件的位置)jquery
resolve.alias:{
'react':patch.resolve(__dirname, './node_modules/react/dist/react.min.js')
}
复制代码
这样会影响Tree-Shaking,适合对总体性比较强的库使用,若是是像lodash这类工具类的比较分散的库,比较适合Tree-Shaking,避免使用这种方式。webpack
默认值:resolve.extensions: ['.js', '.json'],当引入语句没带文件后缀时,webpack会根据extensions定义的后缀列表进行查找,因此: - 列表值尽可能少 - 频率高的文件后缀写在前面 - 代码中引入语句尽量地带上文件后缀,好比require('./data')改写成require('./data.json')。git
好比 JQuery、React,另外若是使用resolve.alias配置了react.min.js,则应该排除解析,由于react.min.js通过构建,已是能够直接运行在浏览器的、非模块化的文件。github
module: {
noParse: [/jquery|lodash, /react\.min\.js$/]
}
复制代码
有殊途同归的效果,就是使用externals外部扩展,剥离第三方依赖模块(如jquery、react、echarts等),不打包到bundle.js。web
module: {
loaders: [{
test: /\.js$/,
loader: 'babel-loader',
include: [
path.resolve(__dirname, "app/src"),
path.resolve(__dirname, "app/test")
],
exclude: /node_modules/
}]
}
复制代码
使用DllPlugin动态连接库插件,大量复用的模块只用编译一次,其原理是把网页依赖的基础模块抽离出来打包到dll文件中,当须要导入模块存在于某个dll中,这个模块再也不打包,直接从dll中获取。我认为使用DllPlugin连接第三方模块,和配置resolve.alias和module.noParse的效果有殊途同归之处。
使用方法:
1)使用DllPlugin插件,配置webpack_dll.config.js来构建dll文件:
const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
module.exports = {
entry: {
// 把 React 相关模块的放到一个单独的动态连接库
react: ['react', 'react-dom'],
// 把项目须要全部的 polyfill 放到一个单独的动态连接库
polyfill: ['core-js/fn/promise', 'whatwg-fetch']
},
output: {
// 输出的动态连接库的文件名称
filename: '[name].dll.js',
// 输出的文件都放到 dist 目录下
path: path.resolve(__dirname, 'dist'),
// 存放动态连接库的全局变量名称,例如对应 react 来讲就是 _dll_react
// 之因此在前面加上 _dll_ 是为了防止全局变量冲突
library: '_dll_[name]'
},
plugins: [
new DllPlugin({
// 动态连接库的全局变量名称
name: '_dll_[name]',
// 描述动态连接库的 manifest.json 文件输出时的文件名称
path: path.join(__dirname, 'dist', '[name].manifest.json')
})
]
}
复制代码
构建输出的如下这四个文件:
├── polyfill.dll.js
├── polyfill.manifest.json
├── react.dll.js
└── react.manifest.json
2)在主config文件,使用DllReferencePlugin插件引入xx.manifest.json动态连接库文件:
const path = require('path');
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
module.exports = {
entry: {
main: './main.js'
},
// ... 省略output和loader配置
plugins: [
new DllReferencePlugin({
// 描述 react 动态连接库的文件内容
manifest: require('./dist/react.manifest.json'),
}),
new DllReferencePlugin({
// 描述 polyfill 动态连接库的文件内容
manifest: require('./dist/polyfill.manifest.json'),
})
]
}
复制代码
ParallelUglifyPlugin插件能够开启多个子进程,每一个子进程使用uglifyJsPlugin压缩代码,能够并行执行,能显著缩短压缩代码时间。
使用方法:
1)安装 webpack-parallel-uglify-plugin 插件:
npm install -D webpack-parallel-uglify-plugin
复制代码
2)而后在webpack.config.js 配置代码以下:
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
module.exports = {
plugins: [
new ParallelUglifyPlugin({
uglifyJS: {
// 这里放uglifyJs的参数
}
})
]
}
复制代码
html-webpack-plugin插件,给html文件载入时添加loading图。使用方法以下:
1)安装 html-webpack-plugin 插件:
npm install -D html-webpack-plugin
复制代码
2)webpack.config.js配置以下:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const loading = require('./render-loading');// 事先设计好的loading图
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
loading: loading
})
]
}
复制代码
prerender-spa-plugin插件,预渲染极大地提升了首屏加载速度。其原理是此插件在本地模拟浏览器环境,预先执行咱们打包的文件,返回预先解析的首屏html。使用方法入以下:
1)安装 prerender-spa-plugin 插件:
npm install -D prerender-spa-plugin
复制代码
2)webpack.config.js配置以下:
const PrerenderSPAPlugin = require('prerender-spa-plugin');
module.exports = {
plugins: [
new PrerenderSPAPlugin({
// 生成文件的路径,也能够与webpakc打包的一致。
staticDir: path.join(__dirname, '../dist'),
// 要预渲染的路由
route: [ '/', '/team', '/analyst','/voter','/sponsor'],
// 这个很重要,若是没有配置这段,也不会进行预编译
renderer: new Renderer({
headless: false,
renderAfterDocumentEvent: 'render-active',
// renderAfterTime: 5000
})
})
]
}
复制代码
3)项目入口文件main.js启动预渲染:
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
i18n,
components: { App },
template: '<App/>',
render: h => h(App),
/* 这句很是重要,不然预渲染将不会启动 */
mounted () {
document.dispatchEvent(new Event('render-active'))
}
})
复制代码
会分析JS代码语法树,理解代码的含义,从而去掉无效代码、日志输出代码,缩短变量名,进行压缩等优化。使用UglifyJSPlug配置webpack.config.js以下:
const UglifyJSPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
//...
plugins: [
new UglifyJSPlugin({
compress: {
warnings: false, //删除无用代码时不输出警告
drop_console: true, //删除全部console语句,能够兼容IE
collapse_vars: true, //内嵌已定义但只使用一次的变量
reduce_vars: true, //提取使用屡次但没定义的静态值到变量
},
output: {
beautify: false, //最紧凑的输出,不保留空格和制表符
comments: false, //删除全部注释
}
})
]
复制代码
现在愈来愈多的浏览器支持直接执行ES6代码了,这样比起转换后的ES5代码量更少,且性能更好。直接运行的ES6代码,也是须要代码压缩,第三方uglify-webpack-plugin提供了压缩ES6代码的功能,使用方法以下:
a、安装uglify-webpack-plugin插件:
uglify-webpack-plugin
复制代码
b、webpack.config.js配置以下:
const UglifyESPlugin = require('uglify-webpack-plugin');
//...
plugins:[
new UglifyESPlugin({
uglifyOptions: { //比UglifyJS多嵌套一层
compress: {
warnings: false,
drop_console: true,
collapse_vars: true,
reduce_vars: true
},
output: {
beautify: false,
comments: false
}
}
})
]
复制代码
另外要防止babel-loader转换ES6代码,要在.babelrc中去掉babel-preset-env,由于正是babel-preset-env负责把ES6转换为ES5。
将js里面分离出来的多个css合并成一个,而后进行压缩、去重等处理。
a、安装引入mini-css-extract-plugin、optimize-css-assets-webpack-plugin插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
复制代码
b、配置loader
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
plugins: [
require('postcss-import')(),
require('autoprefixer')({
browsers: ['last 30 versions', "> 2%", "Firefox >= 10", "ie 6-11"]
})
]
}
}
]
}
]
}
复制代码
c、将多个css文件合并成单一css文件
主要是针对多入口,会产生多分样式文件,合并成一个样式文件,减小加载次数 配置以下
optimization:{
splitChunks: {
chunks: 'all',
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
name: true,
cacheGroups: {
styles: {
name: 'style',
test: /\.css$/,
chunks: 'all',
enforce: true
}
}
}
}
复制代码
- filename 与output中的filename 命名方式同样
- 这里是将多个css合并成单一css文件, 因此chunkFilename 不用处理
- 最后产生的样式文件名大概张这样 style.550f4.css ;style 是 splitChunks-> cacheGroups-> name
new MiniCssExtractPlugin({
filename: 'assets/css/[name].[hash:5].css'
})
复制代码
d、优化css文件,去重压缩等处理
- 主要使用 optimize-css-assets-webpack-plugin 插件和 cssnano 优化器
- cssnano 优化器具体作了哪些优化,可参考 官网
配置方式有两种,效果等同。
方式一:
module.exports = {
optimization:{
minimizer: [
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano'),
// cssProcessorOptions: cssnanoOptions,
cssProcessorPluginOptions: {
preset: ['default', {
// 对注释的处理
discardComments: {
removeAll: true,
},
// 建议设置为false,不然在使用 unicode-range 的时候会产生乱码
normalizeUnicode: false
}]
},
// 是否打印处理过程当中的日志
canPrint: true
})
]
}
}
复制代码
方式二:
module.exports = {
plugins:[
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano'),
// cssProcessorOptions: cssnanoOptions,
cssProcessorPluginOptions: {
preset: ['default', {
discardComments: {
removeAll: true,
},
normalizeUnicode: false
}]
},
canPrint: true
})
]
}
复制代码
Tree Shaking能够剔除用不上的死代码,它依赖ES6的import、export的模块化语法,最早在Rollup中出现,Webpack 2.0将其引入。适合用于Lodash、utils.js等工具类较分散的文件。它正常工做的前提是代码必须采用ES6的模块化语法,由于ES6模块化语法是静态的(在导入、导出语句中的路径必须是静态字符串,且不能放入其余代码块中)。若是采用了ES5中的模块化,例如:module.export = {...}、require( x+y )、if (x) { require( './util' ) },则Webpack没法分析出能够剔除哪些代码。
如何启用Tree Shaking:
{
"presets": [
[
"env",
{ "module": false }, //关闭Babel的模块转换功能,保留ES6模块化语法
]
]
}
复制代码
prepack-webpack-plugin插件能提早计算,代码运行时直接获取结果,提高代码运行速度。其原理是,编译代码时提早将计算结果放到编译后的代码中,而不是运行时才去求值计算,运行代码时直接将运算结果输出以提高性能。prepack的使用方法:
1)安装prepack-webpack-plugin插件:
npm install -D prepack-webpack-plugin
复制代码
2)webpack.config.js配置以下:
const PrepackWebpackPlugin = require('prepack-webpack-plugin').default;
const configuration = {};
module.exports = {
// ...
plugins: [
new PrepackWebpackPlugin(configuration)
]
};
复制代码
Scope Hoisting是Webpack3.x内置的功能,它分析模块间的依赖关系,尽量将被打散的模块合并到一个函数中,但不能形成代码冗余,因此只有被引用一次的模块才能被合并。因为须要分析模块间的依赖关系,因此项目代码中需使用ES6模块化,不然Webpack会降级处理不采用Scope Hoisting。Scope Hoisting的使用配置以下:
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
module.exports = {
// ...
plugins: [
new ModuleConcatenationPlugin()
]
}
复制代码
CDN经过将资源部署到世界各地,使得用户能够就近访问资源,加快访问速度。要接入CDN,须要把网页的静态资源上传到CDN服务上,在访问这些资源时,使用CDN服务提供的URL。
因为CDN会为资源开启长时间的缓存,例如用户从CDN获取index.html,即便以后替换了index.html,用户那边仍会在使用以前的版本直到缓存时间过时。业界的作法:
dist
|-- app_9d89c964.js
|-- app_a6976b6d.css
|-- arch_ae805d49.png
|-- index.html
复制代码
另外,HTTP1.x版本的协议下,浏览器会对于同一个域名并行发起的请求限制在4-8个。那么把全部静态资源放在同一域名下的CDN服务上就会遇到限制,因此能够把静态资源分散在不一样的CDN服务上,例如JS文件放在js.cdn.com域名下,CSS文件放在css.cdn.com域名,图片文件放在img.cdn.com域名下。使用了多个域名后又会带来一个新问题:增长域名解析时间。是否采用多域名分散资源须要根据本身的需求去衡量得失。 固然你能够经过在HTML HEAD标签中加入<link rel="dns-prefetch" href="//js.cdn.com">
去预解析域名,以下降域名解析带来的延迟。
Webpack接入CDN主要的配置以下:
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const {WebPlugin} = require('web-webpack-plugin');
module.exports = {
// 省略 entry 配置...
output: {
// 给输出的 JavaScript 文件名称加上 Hash 值
filename: '[name]_[chunkhash:8].js',
path: path.resolve(__dirname, './dist'),
// 指定存放 JavaScript 文件的 CDN 目录 URL
publicPath: '//js.cdn.com/id/',
},
module: {
rules: [
{
// 增长对 CSS 文件的支持
test: /\.css$/,
// 提取出 Chunk 中的 CSS 代码到单独的文件中
use: ExtractTextPlugin.extract({
// 压缩 CSS 代码
use: ['css-loader?minimize'],
// 指定存放 CSS 中导入的资源(例如图片)的 CDN 目录 URL
publicPath: '//img.cdn.com/id/'
}),
},
{
// 增长对 PNG 文件的支持
test: /\.png$/,
// 给输出的 PNG 文件名称加上 Hash 值
use: ['file-loader?name=[name]_[hash:8].[ext]'],
},
// 省略其它 Loader 配置...
]
},
plugins: [
// 使用 WebPlugin 自动生成 HTML
new WebPlugin({
// HTML 模版文件所在的文件路径
template: './template.html',
// 输出的 HTML 的文件名称
filename: 'index.html',
// 指定存放 CSS 文件的 CDN 目录 URL
stylePublicPath: '//css.cdn.com/id/',
}),
new ExtractTextPlugin({
// 给输出的 CSS 文件名称加上 Hash 值
filename: `[name]_[contenthash:8].css`,
}),
// 省略代码压缩插件配置...
],
};
复制代码
大型网站一般是由多个页面组成,确定会依赖一样的样式文件、脚本文件等。若是不把这些公共文件提取出来,那么每一个单页打包出来的chunck中都会包含公共代码,至关于要传输n份重复代码。若是把公共代码提取成一个文件,那么当用户访问了一个网页加载了这个公共文件,再访问其余依赖公共文件的网页,就直接使用文件在浏览器的缓存,不用重复加载请求。
a、把多个页面依赖的公共代码提取到common.js
const CommonsPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
module.exports = {
plugins:[
new CommonsChunkPlugin({
chunks:['a','b'], //从哪些chunk中提取
name:'common', // 提取出的公共部分造成一个新的chunk
})
]
}
复制代码
b、找出依赖的基础库,写一个base.js文件,再与common.js提取公共代码到base中,common.js就剔除了基础库代码,而base.js保持不变。
//base.js
import 'react';
import 'react-dom';
import './base.css';
//webpack.config.json
module.exports = {
entry:{
base: './base.js'
},
plugins:[
new CommonsChunkPlugin({
chunks:['base','common'],
name:'base',
//minChunks:2, 表示文件要被提取出来须要在指定的chunks中出现的最小次数,防止common.js中没有代码的状况
})
]
}
复制代码
c、获得基础库代码base.js,不含基础库的公共代码common.js,和页面各自的代码文件xx.js。
页面引用顺序以下:base.js--> common.js--> xx.js
在webpack编译完以后,你可能会注意到有一些很小的 chunk - 这产生了大量 HTTP 请求开销。幸运的是使用LimitChunkCountPlugin插件能够经过合并的方式,处理 chunk,以减小http请求数。
const LimitChunkCountPlugin = require('webpack/lib/optimize/LimitChunkCountPlugin');
module.exports = {
// ...
plugins: [
new LimitChunkCountPlugin({
// 限制 chunk 的最大数量,必须大于或等于1的值
maxChunks: 10,
// 设置 chunk 的最小大小
minChunkSize: 2000
})
]
}
复制代码
监听文件有两种方式:
方式一:
在配置文件 webpack.config.js 中设置 watch: true。
// 从配置的 Entry 文件出发,递归解析出 Entry 文件所依赖的文件,
// 把这些依赖的文件加入到监听列表
// 而不是直接监听项目目录下的全部文件
module.export = {
// 只有在开启监听模式时,watchOptions 才有意义
// 默认为 false,也就是不开启
watch: true,
// 监听模式运行时的参数
// 在开启监听模式时,才有意义
watchOptions: {
// 不监听的文件或文件夹,支持正则匹配
// 默认为空
ignored: /node_modules/,
// 在 Webpack 中监听一个文件发生变化的原理是定时的不停的去获取文件的最后编辑时间,
// 每次都存下最新的最后编辑时间,若是发现当前获取的和最后一次保存的最后编辑时间不一致,
// 就认为该文件发生了变化。
// poll 就是用于控制定时检查的周期,具体含义是每隔多少毫秒检查一次
// 默认每隔1000毫秒询问一次
poll: 1000,
// 监听到文件发生变化时,webpack 并不会马上告诉监听者,
// 而是先缓存起来,收集一段时间的变化后,再一次性告诉监听者
// aggregateTimeout 就是用于配置这个等待时间,
// 目的是防止文件更新太快致使从新编译频率过高,让程序构建卡死
// 默认为 300ms
aggregateTimeout: 300,
// 不监听的 node_modules 目录下的文件
ignored: /node_modules/,
}
}
复制代码
方式二:
在执行启动 Webpack 命令时,带上 --watch 参数,完整命令是 webpack --watch。
方式一:webpack-dev-server
在使用 webpack-dev-server 模块去启动 webpack 模块时,webpack 模块的监听模式默认会被开启。 webpack 模块会在文件发生变化时告诉 webpack-dev-server 模块。
方式二:koa + webpack-dev-middleware + webpack-hot-middleware先后端同构
模块热替换(HMR - Hot Module Replacement)功能会在应用程序运行过程当中替换、添加或删除模块,而无需从新加载整个页面,因此预览反应更快,等待时间更少。原理是向每一个chunk中注入代理客户端来链接DevServer和网页。开启方式:
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
module.exports = {
plugins: [
new HotModuleReplacementPlugin()
]
}
复制代码
开启后,若是修改子模块就能够实现局部刷新,但若是修改的是根JS文件,会整个页面刷新。缘由在于,子模块更新时,事件一层层向上传递,直到某个层的文件接收了当前变化的模块,而后执行回调函数。若是一层层向外抛到最外层都没有文件接收,就会刷新整页。
永远不要在生产环境(production)下启用 HMR。
在使用webpack构建前端项目中,逐渐暴露出一些性能问题,其主要有以下几个方面:
针对如上问题,上述webpack优化方案便派上用场了。做为开发工程师,咱们要不断追求项目工程高性能,秉持“什么方案解决什么问题”的准则,针对实际开发项目,持续改进优化项目性能,不断提高开发效率、下降资源成本。