面试官:如何进行前端性能优化?
- 做优化应该是一个有指标、有比较、有数据的过程
- 一个完整的解决方案应该是说清楚标准,讲清楚缘由,算清楚结果,最后用数据与收益来说明工作成果
一、建立衡量标准
在确认指标之后,还要有量化基础,有数据积累。
理论基础 - 以『用户为核心』的 RAIL 性能模型
- Response(响应):应该尽可能快速的响应给客户,应该在 100ms 或者 100ms 以内响应用户输入;
- Animation(动画):在展示动画的时候,每一帧应该以 16ms 进行渲染,这样可以保持动画效果的一次性,并且避免卡顿;
- Idle(浏览器空闲时间):当使用 JavaScript 主线程时,应该把任务划分到执行时间小于 50ms 的片段中,这样可以释放线程以进行用户交互;
- Load(加载):应该在 1s 的时间内加载完你的网站,并可以进行用户交互。
衡量工具
- Chrome DevTools
- Performance:可查看性能指标,并有网页快照。
- Network:可以查看各个资源的加载时间。
- Lighthouse:非常流行的第三方性能评测工具,支持移动端和 PC。
在报告中会对诸如初次内容渲染、可交互时间、加载等进行具体的数值量化打分。
需要注意:
- Lighthouse 并不能真实的反应出每个用户的设备的实际性能数据;
- Lighthouse 的分数反应的是业界的标准,而非项目实际需求的标准。 配置:Chrome ➡️ 开发者工具 ➡️ Lighthouse ➡️ generate report。
- WebPageTest:在线测量和分析网页性能。
- PageSpeed Insights:在线评估网站性能和加载速度。
性能指标
- Navigation Timing API:浏览器提供的 JS API,用于在客户端测量网页加载的性能。
- Lighthouse Performance
- FCP(First Contentful Paint):记录首次加载并绘制内容的时间。
- TTI(Time to Interact):页面可交互的时间。通常通过记录
window.performance.timing
中的loadEventStart
与fetchStart
的时间差来完成。 - LCP(Lagest Contentful Paint):最大可见元素绘制。
- TBT(Total Blocking Time):指从一个请求发送到接收到响应所花费的时间,通常以毫秒为单位。
- CLS(Cumulative Layout Shift):衡量在网页的整个生命周期内发生的所有意外布局偏移的得分总和。得分是零到任意正数,其中 0 表示无偏移,且数字越大,网页的布局偏移越大。为了提供良好的用户体验,网站应努力使 CLS 得分不超过0.1。
二、制定优化方案
我们可以从以下几方面进行优化。
- DNS 解析
- TCP 连接优化
- 请求优化
- 页面渲染优化
- JS 优化
- CSS 优化
- React 优化
- 图片优化
- Webpack 打包优化
DNS 解析
1. DNS 预解析
通过使用dns-prefetch
DNS 预解析,提前获取 IP 地址。
<link rel="dns-prefetch" href="xxx" />
在工程化环境下,写一个脚本文件,读取打包结果,提取代码中的外链接,进行 DNS 预解析。
const fs = require("fs");
const path = require("path");
const { parse } = require("node-html-parser");
const { glob } = require("glob");
const urlRegex = require("url-regex");
const { strict } = require("assert");
// 获取外部链接的正则表达式
const urlPattern = /(https?:\/\/[^/]*)/i;
const urls = new Set();
// 遍历 dist 目录中的所有 HTML、js、css 文件
async function searchDomain() {
const files = (await glob("build/**/*.{html,css,js}")) || [];
for (const file of files) {
const source = fs.readFileSync(file, "utf-8");
const matches = source.match(urlRegex({ strict: true }));
if (matches) {
matches.forEach((url) => {
const match = url.match(urlPattern);
if (match && match[1]) {
urls.add(match[1]);
}
});
}
}
}
// 读取域名,生成 link 元素,加入到打包结果的 head 元素中
async function insertLinks() {
const files = (await glob("build/*.html")) || [];
const links = [...urls]
.map((url) => `<link rel="dns-prefetch" href="${url}">`)
.join("\n");
for (const file of files) {
const html = fs.readFileSync(file, "utf-8");
const root = parse(html);
const head = root.querySelector("head");
head.insertAdjacentHTML("afterbegin", links);
fs.writeFileSync(file, root.toString());
}
}
async function main() {
await searchDomain();
// 在 head 标签中添加预取链接
await insertLinks();
}
main();
在webpack打包时执行上述文件,就可以实现批量 DNS 预解析。
2. 域名收敛
减少页面中域名的数量,从而减少 DNS 解析次数。
TCP 连接优化
1. 使用preconnect
提前和目标服务器进行连接
这个过程包括 DNS 查询得到 IP、TCP 三次握手、HTTP 或 HTTPS 连接。
<link rel="preconnect" href="xxx" />
请求优化
1. 可以使用 http/2 协议
依赖 http/2 的多路复用、头部压缩、二进制传输、服务端推送等特性,从而加快整体请求的响应速度,加快页面的渲染展示。
-
多路复用 多路复用允许同时通过单一的 http/2 连接发送多重请求-响应信息。这个功能相当于是长连接的增强,每个 request 请求可以随机的混杂在一起,接收方可以根据 request 的 id 将 request 再归属到各自不同的服务端请求里。 另外,多路复用也支持了流的优先级,允许客户端告诉服务器哪些内容是更高优先级的资源,可以优先传输。 改善了:在 http1.1 中,浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制(连接数量),超过限制会被阻塞。
-
头部压缩 HTTP1.x 的 header 带有大量信息,而且每次都要重复发送。 HTTP2.0 使用 HPACK 算法对 header 的数据进行压缩,减少需要传输的 header 大小,通讯双方各自缓存一份 header fields 表,差量更新 HTTP 头,既避免了重复 header 的传输,又减小了需要传输的大小。
-
二进制传输 HTTP1.0 的解析是基于文本的,HTTP 2.0 会将所有的传输信息分割为更小的信息或者帧,并对它们进行二进制编码,基于二进制可以让协议有更多的扩展性,比如使用帧来传输数据和指令。 HTTP2.0 在应用层(HTTP2.0)和传输层(TCP/UDP)之间增加一个二进制分帧层。在不改动 HTTP1.x 的语义、方法、状态码、URI 以及首部字段的情况下,解决了 HTTP 1.1 的性能限制,改进传输性能,实现低延迟和高吞吐量。 在二进制分帧层中,HTTP2.0 会将所有传输的信息分割为更小的消息和帧(frame),并对它们采用二进制格式的编码,其中 HTTP1.x 的首部信息会被封装到 HEADER frame,而相应的 Request Body 则封装到 DATA frame 里面。
帧: HTTP 2.0 数据通信的最小单位信息(指 HTTP 2.0 中逻辑上的 HTTP 信息)。例如请求和响应等,消息由一个或多个帧组成。 流: 存在于连接中的一个虚拟通道。流可以承载双向消息,每个流都有一个唯一的整数 ID。
-
服务端推送 服务端推送是一种在客户端请求之前发送数据的机制。 服务端可以在发送 HTML 页面时主动推送其他资源,而不用等到浏览器解析到相应位置,发起请求再响应。例如,服务端可以主动把 JS、CSS 文件推送给服务端,而不需要客户端解析 HTML 时再发送这些请求。 服务端推送的这些资源被保存在了客户端的某处地方,客户端直接从本地加载这些资源就可以了,不用走网络,速度自然快很多。
2. 静态资源使用 CDN
CDN(内容分发网络,Content Delivery/Distribut Nectwork),是建立并覆盖在承载网之上,由分配在不同区域的边缘节点服务器群组成的分布式网络。
CDN 加速的本质是缓存加速。将服务器上存储的静态内容缓存到 CDN 节点上,当访问这些静态内容时,无需访问服务器源站,就近访问 CDN 节点即可获取相同内容,从而达到加速的效果,同时减轻服务器源站的压力。 简单来说,CDN就是根据用户位置分配最近的资源。
CDN 工作原理
在没有 CDN 时,我们使用域名访问一个站点的路径为:用户提交域名 -> 浏览器对域名进行解析 -> DNS 解析得到目的主机的 IP 地址 -> 根据 IP 地址发出请求 -> 得到请求数据并回复。
使用 CDN 后,DNS 返回的不再是一个 IP 地址,而是一个 CName(Canonical Name)别名记录,指向 CDN 的全局负载均衡。
- 当用户在浏览器上输入 URL 时,经过本地 DNS 系统解析,DNS 系统会最终将域名的解析权交给 CNAME 指向的 CDN 专用 DNS 服务器;
- CDN 专用 DNS 服务器将 CDN 全局负载均衡设备的 IP 地址返回给用户;
- 用户向 CDN 全局负载均衡设备发起内容 URL 访问请求;
- CDN 全局负载均衡设备根据用户 IP 地址以及用户请求的内容 URL,选择一台用户所属区域的 CDN 区域负载均衡设备,告诉用户向这台设备发起请求;
- CDN 区域负载均衡设备会为用户选择一台合适的缓存服务器提供服务,然后 CDN 区域负载均衡设备会向全局负载均衡设备返回一台缓存服务器的 IP 地址(选择的依据包括:根据用户 IP 地址,判断哪一台服务器距用户最近;根据用户所请求的 URL 中携带的内容名称,判断哪一台服务器上有用户所需内容;查询各个服务器当前的负载情况,判断哪一台服务器尚有负载能力等等);
- 全局负载均衡设备把缓存服务器的 IP 地址返回给用户;
- 用户向缓存服务器发起请求,缓存服务器响应用户请求,将用户所需内容返回给用户。如果这台缓存服务器上没有用户想要的内容,而区域负载均衡设备依然将它分配给了用户,那么这台缓存服务器就需要向它的上一层缓存服务器请求内容,直至追溯到网站的源服务器将内容拉回到本地。
4. 请求接口优化
- 接口请求合并
页面渲染优化
1. 骨架屏、Loading 图标
2. 服务端渲染(SSR)、Next.js
3. 虚拟列表
核心原理:整个虚拟列表分为三个区域,上缓冲区(0或2个元素)、可视区、下缓冲区(2个元素),每当我们滚动一个元素离开可视区时,就去掉上缓冲区最上面的一个元素,然后在下缓冲区增加一个元素。
JS 优化
1. 防抖和节流
- 节流
规定在一个单位时间内只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
适用场景: 节流适用于需要控制函数执行频率的场景,比如滚动、按钮点击、提交表单等。
// 手写简化版实现 // 1. 定时器实现(非立即执行版) const throttle = (fn, delay = 500) => { let flag = false; return (...args) => { if(flag) return; flag = true; setTimeout(() => { fn.apply(this, args); flag = false; }, delay); } } // 2. 时间戳实现(立即执行版) const throttle = (fn, delay = 500) => { let preTime = 0; return (...args) => { const nowTime = Date.now(); if(nowTime - preTime >= delay) { preTime = Date.now(); fn.apply(this, args); } } } // 3. 实现参数控制是立即执行还是非立即执行的节流 const throttle = (fn, delay = 500, immediate = false) => { let preTime = immediate ? 0 : Date.now(); return (...args) => { const nowTime = Date.now(); if(nowTime - preTime >= delay) { preTime = Date.now(); fn.apply(this, args); } } } // 4. lodash.throttle (https://www.lodashjs.com/docs/lodash.throttle) const throttle = lodash.throttle(() => fn(), delay, options)
- 防抖
在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时。
适用场景: 防抖适用于需要等待用户停止某个操作后执行的场景,如搜索、输入框变化、联想词等。
// 手写简化版实现 // 1. 定时器实现(非立即执行) const debounce = (fn, delay) => { let timeId = null; return (...args) => { if(timeId) { clearTimeout(timeId); } timeId = setTimeout(() => { fn.apply(this, args); }, delay); } }; // 2. lodash.debounce(https://www.lodashjs.com/docs/lodash.debounce) const debounce = lodash.debounce(() => fun(), delay, options); // 3. 防抖如果需要立即执行,则需要第三个参数 const debounce = function(func, delay, immediate) { let timeId; return function () { let context = this; let args = arguments; if (timeId) clearTimeout(timeId); // timeId 不为null if (immediate) { let callNow = !timeId; // 第一次会立即执行,以后只有事件执行后才会再次触发 timeId = setTimeout(function () { timeId = null; }, delay) if (callNow) { func.apply(context, args); } } else { timeId = setTimeout(function () { func.apply(context, args); }, delay); } } }
CSS 优化
React 优化
- 尽量使用
shouldComponent
、React.PureComponent
、React.memo
,避免不必要的 render。 - 类组件中避免使用内联函数,避免每次调用 render 函数时都要重新创建一个新的函数实例。
// bad <button onClick={() => handleClick()}>button1</button> // good <button onClick={handleClick}>button1</button> const handleClick = () => {};
- 使用
Fragement
标签减少层级,避免额外标记。 - 循环使用
key
,但注意key
值不要设置为index
。<div> { array.map(item => <div key={item.id}>{item.name}</div>) } </div>
- Hook 组件使用
useMemo
缓存值,useCallback
缓存函数。 - 使用
React.lazy
和Suspense
组件来实现大组件按需加载/懒加载。const MyComponent = React.lazy(() => import('./MyComponent')); export const johanAsyncComponent = props => ( <React.Suspense fallback={<Spinner />}> <johanComponent {...props} /> </React.Suspense> );
- 路由懒加载
在使用前端路由时,使用按需加载路由组件,以减少初始加载体积。
const Home = () => import('./Home.vue');
图片优化
1. 选择合适的图片格式
类型 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
JPEG/JPG | 色彩丰富;文件小;无兼容问题 | 有损压缩,反复保存图片质量下降明显;不支持动画;不支持透明 | 色彩丰富的图片/渐变图像 |
PNG | 简单图片尺寸小;无损压缩;支持透明 | 不支持动画;色彩丰富的图片尺寸大 | logo、icon、透明图 |
GIF | 文件小;支持动画、透明;无兼容性问题 | 只支持 256 种颜色 | 色彩简单的 logo、icon、动图 |
WebP | 文件小;支持有损和无损压缩;支持动画、透明 | 浏览器兼容性不好 | 支持 webp 格式的 app 和 webview |
2. 图片压缩
- webpack 压缩
# 安装依赖 npm install image-webpack-loader -D
// 配置 webpack module.exports = { ... module: { rules: [ { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, use: [ { loader: 'file-loader', options: { name: '[name].[hash:8].[ext]' }, }, { loader: 'image-webpack-loader', options: { // 用于指定使用 MozJPEG 来压缩 JPEG 图片 mozjpeg: { progressive: true, // 表示是否使用渐进式的方式压缩图片,这意味着在图片加载过程中,用户可以逐步看到清晰度递增的图片 quality: 75, // 质量(1-100),质量越高体积越大,质量越低体积越小;如果使用数组的话,就可以设置 0 到 1 之间的小数 }, // 用于指定使用 OptiPNG 来压缩 PNG 图片 optipng: { enabled: true, // 表示是否启用图片压缩的设置 }, // 用于指定使用 PNGQuant 来压缩 PNG 图片 pngquant: { quality: [0.5, 0.65], speed: 4, // 指定压缩的速度(0-11),其中 0 为最快但质量最差,11 为最慢但质量最好 }, // 用于指定使用 Gifsicle 来压缩 GIF 图片 gifsicle: { interlaced: false, }, // 不支持WEBP就不要写这一项 webp: { quality: 75 }, }, }, ], }, ], }, }
- 工具压缩
- tinypng:免费、批量、速度快;
- 智图压缩:免费、批量、好用;
- squoosh:在线图片压缩工具;
- compressor:支持 JPG、PNG、SVG、GIF。
3. 使用雪碧图
雪碧图(CSS Sprites),国内也叫 CSS 精灵,是一种 CSS 图像合成技术,主要用于小图片显示。
把诸多小图合成一张大图,用background-position
属性来确定图片位置,可以有效的减少请求个数。
适用于页面多且图片丰富的场景。
在 webpack 中webpack-spritesmith插件提供了自动生成雪碧图的功能,并且可以自动生成对应的样式文件。
4. 使用 iconfont
iconfont(字体图标),通过字体的方式展示图标,多用于渲染图标、简单图形、特殊字体等。 阿里字体图标库
5. 小图片转为 base64 编码字符串
// webpack.prod.js
module.exports = {
modules: {
rules: [
// 图片 — 考虑 base64 编码的情况
{
test: /\.(png|jpg|jpeg|gif)$/,
use: {
use: "url-loader",
options: {
limit: 5 * 1024, // 小于 5kb 的图片用 base64 格式产出;否则,依然延用 file-loader 的形式,产出 url
outputPath: "/img1/", // 打包到 img 目录下
},
},
},
]
}
}
6. 图片懒加载
lazy='loading'
<img src="img.png" lazy="loading" />
- 使用 js 监听页面的滚动
使用 js 实现的主要原理是判断当前图片是否到了可视区域:
- 拿到所有的图片 DOM;
- 遍历每个图片,判断当前图片是否到达可视区与范围;
- 如果到了,就设置
src
属性(页面初始化时,图片地址设置在data-src
属性上); - 绑定
window
的scroll
事件,对其进行事件监听。
<img data-src="真实图片地址" src="初始化图片地址" /> <script> window.addEventListener('scroll', throttle(lazyLoad, 200)); function lazyLoad() { let viewHeight = document.body.clientHeight; // 获取可视区高度 let imgs = document.querySelectorAll('img[data-src]'); imgs.forEach((item) => { if (!item.dataset.src) { return; } // 用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置 let rect = item.getBoundingClientRect(); if (rect.bottom >= 0 && rect.top < viewHeight) { item.src = item.dataset.src; item.removeAttribute('data-src'); } }); } </script>
- 使用交叉观察器IntersectionObserver
IntersectionObserver
是浏览器原生提供的构造函数,可以自动“观察”元素是否可见。IntersectionObserver
接收两个参数:callback
:可见性变化时的回调函数;option
:配置选项,可选。
callback
回调函数。callback
一般会触发两次,一次是目标元素刚刚进入视口(开始可见),一次是完全离开视口(开始不可见)。const imgs = document.querySelectorAll('img[data-src]') const config = { rootMargin: '0px', threshold: 0, }; let observer = new IntersectionObserver((entries, self) => { entries.forEach((entry) => { if (entry.isIntersecting) { let img = entry.target; let src = img.dataset.src; if (src) { img.src = src; img.removeAttribute('data-src'); } // 解除观察 self.unobserve(entry.target); } }) }, config); imgs.forEach((image) => { // 开始观察 observer.observe(image); });
Webpack 打包优化
提高打包速度
1. 优化 babel loader
- 优化 loader 的文件搜索范围
module.exports = { module: { rules: [ { test: /\.js$/, // js 文件才使用 babel loader: ['babel-loader'], include: [resolve('src')], // 只在 src 文件下查找 exclude: /node_modules/ // 不会去查找的文件 } ] } }
- 将 babel 编译过的文件缓存起来
module.exports = { module: { rules: [ { test: /\.js$/, // js 文件才使用 babel use: { loader: 'babel-loader', options: { cacheDirectory: true } }, } ] } }
2. IgnorePlugin:避免引入无用模块
比如,使用 moment
库:import moment from 'moment'
,默认会引入所有语言 js 代码,代码过大,那么如何只引入中文?
// index.js
import 'moment/locale/zh-cn';
// webpack.prod.js
module.exports = {
plugin: [
new webpack.IgnorePlugin({ // 忽略 moment 下的 /locale 目录
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
}),
]
}
3. noParse:避免重复打包
module.exports = {
module: {
noParse: [/react\/.min\.js$/]
}
}
- IgnorePlugin 直接不引入,代码中没有
- noParse 引入,但不打包
4. HappyPack:多进程打包
JS 是单线程的,开启多进程打包。
提高构建速度,特别是对于多核 CPU。
happypack 已经不维护了,可以使用 thread-loader 代替
// webpack.prod.js
const HappyPack = require('happypack');
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ["happyPack/loader?id=babel"], // 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
include: srcPath,
},
]
},
plugins: [
// happyPack 开启多进程打包
new HappyPack({
id: "babel", // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
loaders: ["babel-loader?cacheDirectory"], // 如何处理 .js 文件,用法和 Loader 配置中一样
}),
]
}
5. ParallelUglifyPlugin:多进程压缩 JS
webpack 内置 Uglify 工具压缩 js。 JS 是单线程的,开启多进程压缩更快。 和 happyPack 同理。
关于开启多进程打包:
- 项目较大,打包较慢,开启多进程能提高速度
- 项目较小,打包很快,开启多进程会降低速度(进程开销)
// webpack.prod.js
module.exports = {
plugins: [
// 使用 ParallelUglifyPlugin 并行压缩输出的 js 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
// 还是使用 UglifyJS 压缩,只不过帮助开启了多进程
uglifyJS: {
output: {
beautify: false, // 最紧凑的输出
comments: false, // 删除所有的注释
},
compress: {
drop_console: true, // 删除所有的 console 语句,可以兼容 IE 浏览器
collapse_vars: true, // 内嵌定义了但是只用过一次的变量
reduce_vars: true, // 提取出出现多次,但是没有被定义成变量去引用的静态值
},
},
}),
]
}
/**
* var a = 10;
* var b = 20;
* var c = a + b;
* 会被编译成
* var c = 30;
*/
6. 使用 CDN 加速
- 配置 CDN 的公共路径
module.exports = { output: { filename: "[name].[contenthash:8].js", // name 即多入口时,entry 的 key path: distPath, publicPath: "https://cdn.abc.com", // 修改所有静态文件 url 的前缀(如 cdn 域名) }, module: { rules: [ { test: /\.(png|jpg|jpeg|gif)$/, use: { loader: "url-loader", options: { // 小于 5kb 的图片用 base64 格式产出 // 否则,依然延用 file-loader 的形式,产出 url limit: 5 * 1024, outputPath: "/img1/", // 打包到 img 目录下 publicPath: "http://cdn.abc.com", // 设置图片的 cdn 地址(也可以统一在外面的 ) }, }, } ] } }
- 将打包后的结果(dist 目录)上传到 CDN 服务器上。
缩小打包体积
1. 按需加载
使用 Webpack 等构建工具可以通过代码分割和动态导入来实现按需加载。通过import()
语法,可以在运行时动态加载模块。
const module = import('./module.js');
module.then((module) => {
// 使用加载的模块
});
2. 提取公共代码和第三方代码
// webpack.prod.js
module.exports = {
entry: {
index: path.join(srcPath, 'index.js'),
other: path.join(srcPath, 'other.js')
},
plugins: [
// 多入口 —— 生成 index.html
new HtmlWebpackPlugin({
template: path.join(srcPath, 'index.html'),
filename: 'index.html',
// chunks 表示该页面需要引用哪些 chunk
chunks: ['index', 'vendor', 'common'] // 考虑代码分割
}),
// 多入口 —— 生成 other.html
new HtmlWebpackPlugin({
template: path.join(srcPath, 'other.html'),
filename: 'other.html',
chunks: ['other', 'vendor', 'common']
}),
],
optimization: {
// 分割代码块
splitChunks: {
/**
* initial: 入口 chunks,对于异步导入的文件不处理
* async: 异步 chunk,只对异步导入的文件处理
* all: 全部 chunk
*/
chunks: 'all',
// 缓存分组
cacheGroups: {
// 第三方模块
vendor: {
name: 'vendor', // chunk 名称
priority: 1, // 权限更高,优先抽离,重要!!
test: /node_modules/,
minSize: 0, // 大小限制
minChunks: 1, // 最少复用几次
},
// 公共的模块
common: {
name: 'common', // chunk 名称
priority: 0, // 优先级
minSize: 0, // 公共模块的大小限制
minChunks: 2 // 公共模块最少复用过几次
}
}
}
}
}
3. bundle 加 hash
hash 通常被作为前端静态资源实现增量更新的方案,通过在文件名上带上一串 hash 字符串,告诉浏览器该文件是否发生更新,从而决定是否要使用缓存机制。 Webpack 打包时的 hash 有三种:fullhash(Webpack4.x 之前的叫 hash,Webpack5.x 叫 fullhash 或 hash 都可)、chunkhash 和 contenthash。
在生产环境下,我们对 output 中打包的文件名一般采用 chunkhash,对于 css 等样式文件,采用 contenthash,这样可以使得每个模块最小范围的改变 hash 值。 一方面可以最大程度的利用浏览器缓存机制,提升用户体验; 另一方面,合理利用 hash 也减少了 webpack 再次打包所要处理的文件数量,提升了打包速度。
-
fullhash fullhash 是全量的 hash,是整个项目级别的,只要项目中有任何的一个文件发生变动,打包后所有文件的 hash 值都会改变。
// webpack.config.js module.exports = { output: { path: path.resolve(__dirname, "./dist"), filename: "[name].[hash].js", clean: true, } }
执行
npx webpack
命令进行项目打包。文件改动前打包 文件改动后打包 -
chunkhash chunkhash 根据不同的入口文件(entry)进行依赖文件解析、构建对应的 chunk,生成对应的 hash 值。当某个文件内容发生变动时,再次执行打包,只有该文件以及依赖该文件的文件的打包结果 hash 值会发生改变。
// webpack.config.js module.exports = { output: { path: path.resolve(__dirname, "./dist"), filename: "[name].[chunkhash].js", clean: true, } }
文件改动前打包 文件改动后打包 因为改动了
add.js
文件,所以依赖了这个文件的index.js
文件的 hash 值也会发生改变,但是除此之外index.css
文件的 hash 值也发生了改变,这是因为在index.js
中引用了index.css
,打包后它们属于一个模块。 -
contenthash contenthash 是只有当文件自己的内容发生改变时,其打包的 hash 值才会发生变动。
// webpack.config.js module.exports = { output: { path: path.resolve(__dirname, "./dist"), filename: "[name].[conenthash].js", clean: true, } }
文件改动前打包 文件改动后打包 只有 add.js 和 index.js 文件的 hash 值发生了改变。
4. 使用production
模式
module.exports = {
mode: 'production'
}
- 自动开启压缩
- Vue、React 等会自动删掉测试代码(如开发环境的 waring)
- 启用 Tree Shaking
Tree Shaking
Tree Shaking 可以用来删除项目中未被引用的代码。
5. Scope Hoisting 作用域提升
Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中。
const ModuleConcatenationPlugin = require('webpack/lib/ModuleConcatenationPlugin');
module.exports = {
resolve: {
// 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向 ES6 模块化语法的文件
mainFields: ['jsnext:main', 'browser', 'main']
},
plugins: [
// 开启 Scope Hoisting
new ModuleConcatenationPlugin()
]
}
6. 抽离压缩 CSS 文件
webpack.dev.js
文件配置:
module.exports = {
rules: [
{
test: /\.css$/,
// loader 的执行顺序是从后往前,postcss-loader 是处理浏览器兼容性问题的
use: ['style-loader', 'css-loader', 'postcss-loader']
},
{
test: /\.less$/,
// 增加 less-loader,注意顺序
use: ['style-loader', 'css-loader', 'less-loader']
}
],
}
webpack.prod.js
文件配置:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const TerserJSPlugin = require("terser-webpack-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = {
rules: [
// 抽离 css
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
]
},
// 抽离 less
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
'postcss-loader'
]
}
],
plugins: [
// 抽离 css 文件
new MiniCssExtractPlugin({
filename: 'css/main.[contenthash:8].css'
})
],
optimization: {
// 压缩 css
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugiin({})]
}
}
三、持续跟进
- 性能优化是一个循序渐进的过程,不像 bug 能一次性解决;
- 持续跟进统计结果,再逐渐分析性能瓶颈,持续优化;
- 可使用第三方统计服务,如阿里云 ARMS、百度统计等等。
『性能优化方案』持续更新中,有问题欢迎指正~
转载自:https://juejin.cn/post/7347504184068931619