webpack的性能优化
webpack 是一种前端构建工具(从前期的打包工具逐渐演化为构建工具)。它的配置并不简单,由此演化的优化自成一套很复杂的知识体系。
webpack 性能优化可分为开发环境优化
和生产环境优化
。开发环境优化又分为优化代码构建速度
,和优化代码调试
。生产环境优化又分为优化打包构建速度
和优化代码运行性能
。
webpack打包原理
Webpack 是一个模块打包工具,它能够把各种资源,例如 JavaScript、CSS、图片等都作为模块来处理,并且可以利用插件来扩展其功能,从而实现资源的合并、压缩、替换等。
Webpack 的核心概念包括入口(entry)、加载器(loader)、插件(plugins)和输出(output)。
webpack 五个核心概念
Entry
入口(Entry):指示 webpack 以哪个文件为入口起点开始打包,分析构建内部依赖图。
Output
输出(Output):指示 webpack 打包后的资源 bundles 输出到哪里去,以及如何命名。
Loader
Loader:让 webpack 能够去处理那些非 JS 的文件,比如样式文件、图片文件(webpack 自身只理解
JS)
Plugins
插件(Plugins):可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,
一直到重新定义环境中的变量等。
Mode
模式(Mode):指示 webpack 使用相应模式的配置
Webpack打包过程
读取配置文件:Webpack会首先读取项目中的配置文件,例如webpack.config.js或者webpackfile.js等。
解析入口文件:Webpack会从配置文件中获取入口文件路径,根据入口文件路径解析出入口文件及其依赖的模块。
解析依赖模块:Webpack会逐个解析入口文件依赖的模块,以此类推,逐步构建出完整依赖树。
加载模块:Webpack会根据解析出的模块路径逐个加载模块,支持多种文件格式的加载。
分析模块依赖关系及模块交互:Webpack会分析每个模块之间的依赖关系,例如调用其它模块的函数或变量引用等,以此建立模块之间的交互关系图。
打包模块:Webpack会将所有模块根据依赖树的关系,逐步打包成一个或多个JavaScript文件,支持多种打包方式。
生成输出文件:Webpack会根据配置文件中指定的输出路径和文件名,生成最终的输出文件。
优化打包结果:Webpack还提供了一些优化功能,例如压缩代码、合并模块等,可以进一步提升打包结果的性能和效率。
构建完成:Webpack打包过程完成后,会输出一些统计信息以及警告或错误信息,方便开发者定位和解决问题。
Webpack生命周期:
Webpack 在构建过程中会触发一系列的生命周期事件,开发者可以针对这些事件进行相应的处理或插件化。下面是Webpack的主要生命周期事件:
beforeRun:在 Webpack 开始执行构建任务前触发的事件。
run:Webpack 开始进行编译打包时触发的事件。
beforeCompile:在 Webpack 开始编译之前触发的事件。
compile:Webpack 开始编译时触发的事件。
compilation:在 Webpack 的每次编译构建过程中触发的事件。
emit:在 Webpack 输出资源到output目录之前触发的事件。
afterEmit:在 Webpack 输出资源到output目录之后触发的事件。
done:Webpack 构建完成所有的编译、和输出等任务之后触发的事件。
failed:Webpack 构建过程中出现错误时触发的事件。
-
以下是一个简单的 Webpack 配置示例:
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './src/index.js', // 入口文件
output: {
filename: 'bundle.js', // 输出文件名
path: path.resolve(__dirname, 'dist') // 输出文件路径
},
module: {
rules: [
{
test: /\.css$/, // 正则表达式,匹配 CSS 文件
use: [
'style-loader', // 将 CSS 转换成 JavaScript 可以加载
'css-loader' // 加载 CSS 文件
]
},
{
test: /\.js$/, // 正则表达式,匹配 JS 文件
exclude: /node_modules/, // 排除 node_modules 目录
loader: 'babel-loader' // 转换 ES6 为 ES5
}
]
},
plugins: [
new webpack.optimize.UglifyJsPlugin() // 压缩代码
]
};
一、减少打包时间
优化 Loader
对于 Loader 来说,影响打包效率首当其冲必属 Babel 了。因为 Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后再生成新的代码,项目越大,转换代码越多,效率就越低。当然了,我们是有办法优化的。
首先我们可以优化 Loader 的文件搜索范围,让尽可能少的文件被 Loader 处理。可以通过 test/include/exclude 三个配置项来命中 Loader 要应用规则的文件。
module.exports = {
module: {
rules: [
{
// js 文件才使用 babel
test: /\.js$/,
loader: 'babel-loader',
// 只在 src 文件夹下查找
include: [resolve('src')],
// 不会去查找的路径
exclude: /node_modules/
}
]
}
}
对于 Babel 来说,我们肯定是希望只作用在 JS 代码上的,然后 node_modules 中使用的代码都是编译过的,所以我们也完全没有必要再去处理一遍。
当然这样做还不够,我们还可以将 Babel 编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间。
loader: 'babel-loader?cacheDirectory=true'
HappyPack
受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况
HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了
HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了
安装HappyPack插件:
npm i -D happypack
配置HappyPack:
const HappyPack =require (’happypack’);
// 构造出共享进程池,在进程池中包含 5 个子进程
const happyThreadPool = HappyPack.ThreadPool({ size : 5 )) ;
/**** 使用HappyPack实例化 *****/
module: {
loaders: [
{
test: /\.js$/,
include: [resolve('src')],
exclude: /node_modules/,
// id 后面的内容对应下面
loader: 'happypack/loader?id=happybabel'
}
]
},
plugins: [
new HappyPack({
// 用唯一的标识符id来代表当前的HappyPack 处理一类特定的文件
id: 'happybabel',
loaders: ['babel-loader?cacheDirectory'],
// 开启 4 个线程
threads: 4,
// 使用共享进程池中的子进程去处理任务。
threadPool: happyThreadPool,
// 是否允许HappyPack输出日志,默认为true
verbose: false
})
]
属性解读:
threads: 代表开启几个子进程去处理这一类文件,默认是3个,必须是整数。
verbose: 是否允许HappyPack输出日志,默认为true。
threadPool: 代表共享进程池。即多个HappyPack实列都使用同一个共享进程池中的子进程去处理任务。以防止资源占用过多
DllPlugin
DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。
接下来我们就来学习如何使用 DllPlugin
// 单独配置在一个文件中
// webpack.dll.conf.js
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
// 想统一打包的类库
vendor: ['react']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].dll.js',
library: '[name]-[hash]'
},
plugins: [
new webpack.DllPlugin({
// name 必须和 output.library 一致
name: '[name]-[hash]',
// 该属性需要与 DllReferencePlugin 中一致
context: __dirname,
path: path.join(__dirname, 'dist', '[name]-manifest.json')
})
]
}// 单独配置在一个文件中
// webpack.dll.conf.js
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
// 想统一打包的类库
vendor: ['react']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].dll.js',
library: '[name]-[hash]'
},
plugins: [
new webpack.DllPlugin({
// name 必须和 output.library 一致
name: '[name]-[hash]',
// 该属性需要与 DllReferencePlugin 中一致
context: __dirname,
path: path.join(__dirname, 'dist', '[name]-manifest.json')
})
]
}
然后我们需要执行这个配置文件生成依赖文件,接下来我们需要使用 DllReferencePlugin
将依赖文件引入项目中。
// webpack.conf.js
module.exports = {
// ...省略其他配置
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
// manifest 就是之前打包出来的 json 文件
manifest: require('./dist/vendor-manifest.json'),
})
]
}
代码压缩
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
...
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true // 电脑cpu核数-1
})
]
}
}
属性介绍:
属性 | 描述 |
extractComments | 默认值为true,表示会将注释抽取到一个单独的文件中,开发阶段,我们可设置为 false ,不保留注释 |
parallel | 使用多进程并发运行提高构建的速度,默认值是true,并发运行的默认数量: os.cpus().length - 1 |
terserOptions | 设置我们的terser相关的配置 |
compress | 设置压缩相关的选项,mangle:设置丑化相关的选项,可以直接设置为true |
mangle | 设置丑化相关的选项,可以直接设置为true |
toplevel | 底层变量是否进行转换 |
keep_classnames | 类的名称 |
keep_fnames | 类的名称 |
CSS代码压缩
CSS的压缩我们可以使用另外一个插件:css-minimizer-webpack-plugin
npm install css-minimizer-webpack-plugin -D
如何配置:
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
optimization: {
minimize: true,
minimizer: [
new CssMinimizerPlugin({
parallel: true
})
]
}
}
Html压缩
module.exports = {
...
plugin:[
new HtmlwebpackPlugin({
...
minify:{
minifyCSS:false, // 是否压缩css
collapseWhitespace:false, // 是否折叠空格
removeComments:true // 是否移除注释
}
})
]
}
设置了minify
,实际会使用另一个插件html-minifier-terser。
图片压缩
一般来说在打包之后,一些图片文件的大小是远远要比 js
或者 css
文件要来的大,所以图片压缩较为重要。
如何配置:
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
}
},
{
loader: 'image-webpack-loader',
options: {
// 压缩 jpeg 的配置
mozjpeg: {
progressive: true,
quality: 65
},
// 使用 imagemin**-optipng 压缩 png,enable: false 为关闭
optipng: {
enabled: false,
},
// 使用 imagemin-pngquant 压缩 png
pngquant: {
quality: '65-90',
speed: 4
},
// 压缩 gif 的配置
gifsicle: {
interlaced: false,
},
// 开启 webp,会把 jpg 和 png 图片压缩为 webp 格式
webp: {
quality: 75
}
}
}
]
},
]
}
文件大小压缩
对文件的大小进行压缩,减少http
传输过程中宽带的损耗
安装插件
npm install compression-webpack-plugin -D
new ComepressionPlugin({
test:/\.(css|js)$/, // 哪些文件需要压缩
threshold:500, // 设置文件多大开始压缩
minRatio:0.7, // 至少压缩的比例
algorithm:"gzip", // 采用的压缩算法
})
文件监听
在开启监听模式时,默认情况下会监听配置的 Entry 文件和所有 Entry 递归依赖的文件,在这些文件中会有很多存在于 node_modules 下,因为如今的 Web 项目会依赖大量的第三方模块, 所以在大多数情况下我们都不可能去编辑 node_modules 下的文件,而是编辑自己建立的源码文件,而一个很大的优化点就是忽略 node_modules 下的文件,不监听它们。
module.export = {
watchOptions : {
//不监听的 node_modules 目录下的文件
ignored : /node_modules/,
}
} module.export = {
watchOptions : {
//不监听的 node_modules 目录下的文件
ignored : /node_modules/,
}
}
采用这种方法优化后, Webpack 消耗的内存和 CPU 将会大大减少。
小优化点
resolve.extensions
用来表明文件后缀列表,默认查找顺序是 ['.js', '.json']
,如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面。
module.exports = {
resolve : {
//尽可能减少后缀尝试的可能性
extensions : ['js'],
}
}
resolve.alias
resolve.alias 配置项通过别名来将原导入路径映射成一个新的导入路径。在实战项目中经常会依赖一些庞大的第三方模块,以 React 库为例,发布出去的 React 库中包含两套代码
一套是采用 CommonJS 规范的模块化代码,这些文件都放在 lib 录下,以 package.json 中指定的入口文件 react.js 为模块的入口
一套是将 React 的所有相关代码打包好的完整代码放到一个单独的文件中, 这些代码没有采用模块化,可以直接执行。其中 dist/react.js 用于开发环境,里面包含检查和警告的代码。dist/react.min.js 用于线上环境,被最小化了。
在默认情况下, Webpack 会从入口文件 ./node_modules/react/react.js 开始递归解析和处理依赖的几十个文件,这会是一个很耗时的操作 通过配置 resolve.alias, 可以让 Webpack 在处理 React 库时,直接使用单独、完整的 react.min.js 文件,从而跳过耗时的递归解析操作。
module.exports = {
resolve: {
//使用 alias 将导入 react 的语句换成直接使用单独、完整的 react.min.js 文件,
//减少耗时的递归解析操作
alias: {
'react': path.resolve( __dirname ,'./node_modules/react/dist/react.min.js'),
}
}
}
module.noParse
module.noParse 配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。原因是一些库如 jQuery
module.exports = {
module: {
noParse: /jquery/,
}
}; module.exports = {
module: {
noParse: /jquery/,
}
};
二、减少 Webpack 打包后的体积
vue为例实现懒加载:
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [{
path: '/',
name: 'Home',
// 将子组件加载语句封装到一个function中,将function赋给component
component: () => import( /* webpackChunkName: "home" */ '../views/Home.vue')
}
]
const router = new VueRouter({
mode: 'history',
routes
})
export default router
总结:
当然不仅仅路由可以按需加载,对于 loadash 这种大型类库同样可以使用这个功能。按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 Promise,当 Promise 成功以后去执行回调。
Scope Hoisting
Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。
比如我们希望打包两个文件
// test.js
export const a = 1
// index.js
import { a } from './test.js'
对于这种情况,我们打包出来的代码会类似这样
[
/* 0 */
function (module, exports, require) {
//...
},
/* 1 */
function (module, exports, require) {
//...
}
]
但是如果我们使用 Scope Hoisting 的话,代码就会尽可能的合并到一个函数中去,也就变成了这样的类似代码。
[
/* 0 */
function (module, exports, require) {
//...
}
]
这样的打包方式生成的代码明显比之前的少多了。
如何配置:
如果在 Webpack4 中你希望开启这个功能,只需要启用optimization.concatenateModules 就可以了。
module.exports = {
optimization: {
concatenateModules: true
}
}
Tree Shaking
Tree Shaking 可以实现删除项目中未被引用的代码,比如Tree Shaking 可以实现删除项目中未被引用的代码,比如
// test.js
export const a = 1
export const b = 2
// index.js
import { a } from './test.js'
对于以上情况,test
文件中的变量 b
如果没有在项目中使用到的话,就不会被打包到文件中。
如何配置:
如果你使用 Webpack 4 的话,开启生产环境就会自动启动这个优化功能。
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
},
// 其他配置项
};
需要注意:
要确认Webpack版本、使用ES6的语法、使用正确的工具,以及在Webpack配置文件中启用Tree-Shaking。同时,需要注意在Tree-Shaking优化过程中可能会出现的一些问题,例如配合动态导入使用时需要进行额外的配置等
CND加速
CDN 的全称是 Content Delivery Network,即内容分发网络。CDN 是构建在网络之上的内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN 的关键技术主要有内容存储和分发技术。
随着项目越做越大,依赖的第三方 npm 包越来越多,构建之后的文件也会越来越大。再加上又是单页应用,这就会导致在网速较慢或者服务器带宽有限的情况出现长时间的白屏。此时我们可以使用CDN的方法,优化网络加载速度。
配置如下
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const {WebPlugin} = require('web-webpack-plugin');
//...
output:{
filename: '[name]_[chunkhash:8].js',
path: path.resolve(__dirname, 'dist'),
publicPatch: '//js.cdn.com/id/', //指定存放JS文件的CDN地址
},
module:{
rules:[{
test: /\.css/,
use: ExtractTextPlugin.extract({
use: ['css-loader?minimize'],
publicPatch: '//img.cdn.com/id/', //指定css文件中导入的图片等资源存放的cdn地址
}),
},{
test: /\.png/,
use: ['file-loader?name=[name]_[hash:8].[ext]'], //为输出的PNG文件名加上Hash值
}]
},
plugins:[
new WebPlugin({
template: './template.html',
filename: 'index.html',
stylePublicPath: '//css.cdn.com/id/', //指定存放CSS文件的CDN地址
}),
new ExtractTextPlugin({
filename:`[name]_[contenthash:8].css`, //为输出的CSS文件加上Hash
})
]
前端性能优化
1. 代码优化和压缩
确保前端代码经过优化和压缩,以减小文件大小,加快加载速度。可以使用工具如Webpack、Parcel或Rollup进行代码打包和压缩,同时使用代码分割技术来减少首屏需要加载的资源量
2. 懒加载和按需加载
将页面分为多个模块,并使用懒加载技术或按需加载方式,只在需要时加载相应的模块,减少首屏所需加载的资源量和时间。
3. 预加载关键资源
对于首屏必要的关键资源(如CSS、JavaScript、图片等),可以通过预加载技术在页面加载过程中提前加载,以加快首屏渲染速度。
4. SSR(服务器端渲染)
使用服务器端渲染(SSR)技术生成首屏内容,将渲染工作放在服务器端完成,减少客户端的渲染压力和等待时间,从而快速展示首屏内容。
5. 优化图片和多媒体资源
确保图片和多媒体资源经过压缩和优化,以减小文件大小,加快加载速度。可以使用工具如ImageMagick、TinyPNG等进行图片压缩。
6. 减少重定向和请求次数
减少页面重定向和不必要的请求次数,确保页面加载过程中的请求尽可能减少,以提高加载速度。
7. CDN 加速
使用内容分发网络(CDN)加速静态资源的访问速度,将资源分发到全球各地的节点,加快资源加载速度。
8. 使用缓存机制
合理利用浏览器缓存和服务端缓存机制,减少重复加载相同资源,提高页面加载速度。
通过以上方法的组合或者根据具体情况选择其中一种或几种方法,可以有效解决前端首屏白屏问题,提升用户体验。