拯救你的年底KPI:前端性能优化
拯救你的年底 KPI:前端性能优化 🤔🤔🤔
# 前言
性能优化
,每个工程师跑不掉的一个话题。这里是本人结合Mr.Max (opens new window) 的一门课,总结的一些优化手法,希望对大家有所帮助,后续也会继续更新。 演示源码和 PPT 有条件分享(一个赞
👍),感谢各位大哥 😎
# 首发个人博客
种一棵树,最好的时机是十年前,其次是现在 (opens new window)
# 演示 demo
fe-optimize 源码 (opens new window)
# 演示 PPT (一定要看,超帅)
fe-ppt 使用 HTML 做的 PPT 源码 (opens new window)
# 前端性能的影响(长篇大论警告 ❗️❗️❗️)
前端性能的一个重要指标是页面加载时间,不仅事关用户体验,也是搜索引擎排名考虑的一个因素。
- 来自 Google 的数据表明,一个有 10 条数据 0.4 秒能加载完的页面,变成 30 条数据 0.9 秒加载完之后,流量和广告收入下降
90%
。 - Google Map 首页文件大小从
100KB
减小到70-80KB
后,流量在第一周涨了10%
,接下来的三周涨了25%
。 - 亚马逊的数据表明:加载时间增加
100毫秒
,销量就下降1%
。
所以:重铸性能之光,我辈义不容辞 😎
# 一、调试工具
磨刀不误砍柴工,读完大学再打工!
# 1、Network
这里可以看到资源加载详情,初步评估影响页面性能
的因素。鼠标右键可以自定义选项卡,页面底部是当前加载资源的一个概览。DOMContentLoaded
DOM 渲染完成的时间,Load
:当前页面所有资源加载完成的时间
# 思考:如何判断哪些资源对当前页面加载无用,做对应优化?
shift + cmd + P 调出控制台的扩展工具,添加规则
# 扩展工具 更多使用姿势
# 瀑布流 waterfall
Queueing
浏览器将资源放入队列时间Stalled
因放入队列时间而发生的停滞时间DNS Lookup
DNS 解析时间Initial connection
建立 HTTP 连接的时间SSL
浏览器与服务器建立安全性连接的时间TTFB
等待服务端返回数据的时间Content Download
浏览器下载资源的时间
# 2、Lighthouse
First Contentful Paint
首屏渲染时间,1s 以内绿色Speed Index
速度指数,4s 以内绿色Time to Interactive
到页面可交换的时间
根据 chrome 的一些策略自动对网站做一个质量评估,并且会给出一些优化的建议。
# 3、Peformance
对网站最专业的分析~后面会多次讲到
# 4、webPageTest
可以模拟不同场景下访问的情况,比如模拟不同浏览器、不同国家等等,在线测试地址:webPageTest (opens new window)
# 5、资源打包分析
# webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer
// webpack.config.js 文件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports={
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'server',
analyzerHost: '127.0.0.1',
analyzerPort: 8889,
reportFilename: 'report.html',
defaultSizes: 'parsed',
openAnalyzer: true,
generateStatsFile: false,
statsFilename: 'stats.json',
statsOptions: null,
logLevel: 'info'
}),
]
}
// package.json
"analyz": "NODE_ENV=production npm_config_report=true npm run build"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 开启 source-map
webpack.config.js
module.exports = {
mode: 'production',
devtool: 'hidden-source-map',
}
2
3
4
package.json
"analyze": "source-map-explorer 'build/*.js'",
npm run analyze
# 二、WEB API
工欲善其事,必先利其器。浏览器提供的一些分析 API至关重要
# 1、监听视窗激活状态
大学都刷过慕课吧?只要离开窗口视频就会暂停~
或者一些考试网站,提醒你不能离开当前窗口
再或者,这种效果~
// 窗口激活状态监听
let vEvent = 'visibilitychange';
if (document.webkitHidden != undefined) {
vEvent = 'webkitvisibilitychange';
}
function visibilityChanged() {
if (document.hidden || document.webkitHidden) {
document.title = '客官,别走啊~'
console.log("Web page is hidden.")
} else {
document.title = '客官,你又回来了呢~'
console.log("Web page is visible.")
}
}
document.addEventListener(vEvent, visibilityChanged, false);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
其实有很多隐藏的 api,这里大家有兴趣的可以去试试看:
# 2、观察长任务(performance 中 Task)
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry)
}
})
observer.observe({entryTypes: ['longtask']})
2
3
4
5
6
7
# 3、监听网络变化
网络变化时给用户反馈网络问题,有时候看直播的时候自己的网络卡顿,直播平台也会提醒你或者自动给你切换清晰度
var connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
var type = connection.effectiveType;
function updateConnectionStatus() {
console.log("Connection type changed from " + type + " to " + connection.effectiveType);
type = connection.effectiveType;
}
connection.addEventListener('change', updateConnectionStatus);
2
3
4
5
6
7
8
9
# 4、计算 DOMContentLoaded 时间
window.addEventListener('DOMContentLoaded', (event) => {
let timing = performance.getEntriesByType('navigation')[0];
console.log(timing.domInteractive);
console.log(timing.fetchStart);
let diff = timing.domInteractive - timing.fetchStart;
console.log("TTI: " + diff);
})
2
3
4
5
6
7
# 5、更多计算规则
DNS 解析耗时: domainLookupEnd - domainLookupStart
TCP 连接耗时: connectEnd - connectStart
SSL 安全连接耗时: connectEnd - secureConnectionStart
网络请求耗时 (TTFB): responseStart - requestStart
数据传输耗时: responseEnd - responseStart
DOM 解析耗时: domInteractive - responseEnd
资源加载耗时: loadEventStart - domContentLoadedEventEnd
First Byte时间: responseStart - domainLookupStart
白屏时间: responseEnd - fetchStart
首次可交互时间: domInteractive - fetchStart
DOM Ready 时间: domContentLoadEventEnd - fetchStart
页面完全加载时间: loadEventStart - fetchStart
http 头部大小: transferSize - encodedBodySize
重定向次数:performance.navigation.redirectCount
重定向耗时: redirectEnd - redirectStart
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 三、老生常谈,雅虎军规
磨好刀了,就该想想往哪里捅比较好了~ 🗡🗡🗡
关于雅虎军规,你知道的有多少条,平时写用到的又有哪些?针对以下规则,我们可以做很多优化工作
# 1、减少 cookie 传输
cookie 传输会造成带宽浪费,可以:
- 减少 cookie 中存储的东西
- 静态资源不需要 cookie,可以采用其他的域名,不会主动带上 cookie。
# 2、避免过多的回流与重绘
连续触发页面回流操作
let cards = document.getElementsByClassName("MuiPaper-rounded");
const update = (timestamp) => {
for (let i = 0; i <cards.length; i++) {
let top = cards[i].offsetTop;
cards[i].style.width = ((Math.sin(cards[i].offsetTop + timestamp / 100 + 1) * 500) + 'px')
}
window.requestAnimationFrame(update)
}
update(1000);
2
3
4
5
6
7
8
9
看下效果,很明显的卡顿
performance
分析结果,load
事件之后存在大量的回流,并且chrome
都给标记了红色
使用fastDom
进行优化,将对 dom 的读和写
分离,合并
let cards = document.getElementsByClassName("MuiPaper-rounded");
const update = (timestamp) => {
for (let i = 0; i < cards.length; i++) {
fastdom.measure(() => {
let top = cards[i].offsetTop;
fastdom.mutate(() => {
cards[i].style.width =
Math.sin(top + timestamp / 100 + 1) * 500 + "px";
});
});
}
window.requestAnimationFrame(update)
}
update(1000);
2
3
4
5
6
7
8
9
10
11
12
13
14
再看下效果,很流畅~
performance
分析结果,load 事件之后也没有了那么多的红色标记
感兴趣的可以去了解一下 fastDom:github fastdom (opens new window) 在线预览:fastdom demo (opens new window)
关于任务拆分与组合的思想,react fiber
架构做的很牛逼,有兴趣的可以去了解一下调度算法在 fiber 中的实践
# 四、压缩
嗯哼哼、确定一下没有走错场子,继续继续!
# 1、Gzip
开启方式可参考:nginx 开启 gzip (opens new window)
还有一种方式:打包的时候生成 gz 文件,上传到服务器端,这样就不需要 nginx 来压缩了,可以降低服务器压力。 可参考:gzip 压缩文件&webPack 配置 Compression-webpack-plugin (opens new window)
# 2、服务端压缩
server.js
const express = require('express');
const app = express();
const fs = require('fs');
const compression = require('compression');
const path = require('path');
app.use(compression());
app.use(express.static('build'));
app.get('*', (req,res) =>{
res.sendFile(path.join(__dirname+'/build/index.html'));
});
const listener = app.listen(process.env.PORT || 3000, function () {
console.log(`Listening on port ${listener.address().port}`);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package.json
"start": "npm run build && node server.js",
# 3、JavaScript、Css、Html 压缩
工程化项目中直接使用对应的插件即可,webpack 的主要有下面三个:
- UglifyJS
- webpack-parallel-uglify-plugin
- terser-webpack-plugin
具体优缺点可参考:webpack 常用的三种 JS 压缩插件 (opens new window)。
压缩原理
简单的讲就是去除一些空格、换行、注释,借助 es6 模块化的功能,做了一些tree-shaking
的优化。同时做了一些代码混淆,一方面是为了更小的体积,另一方面也是为了源码的安全性。
css 压缩主要是 mini-css-extract-plugin,当然前面的 js 压缩插件也会给你做好 css 压缩。使用姿势:
npm install --save-dev mini-css-extract-plugin
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
plugins:[
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css"
})
]
2
3
4
5
6
7
8
html 压缩可以用HtmlWebpackPlugin
,单页项目就一个 index.html,性能提升微乎其微~
# 4、http2 首部压缩
http2 的特点
- 二进制分帧
- 首部压缩
- 流量控制
- 多路复用
- 请求优先级
- 服务器推送
http2_push: 'xxx.jpg'
具体升级方式也很简单,修改一下 nginx 配置,方法请自行Google
# 五、webpack 优化
上文中也提到了部分 webpack 插件,下面我再来看看还有哪些~
# 1、DllPlugin 提升构建速度
通过DllPlugin
插件,将一些比较大的,基本很少升级的包拆分出来,生成xx.dll.js
文件,通过manifest.json
引用
webpack.dll.config.js
const path = require("path");
const webpack = require("webpack");
module.exports = {
mode: "production",
entry: {
react: ["react", "react-dom"],
},
output: {
filename: "[name].dll.js",
path: path.resolve(__dirname, "dll"),
library: "[name]"
},
plugins: [
new webpack.DllPlugin({
name: "[name]",
path: path.resolve(__dirname, "dll/[name].manifest.json")
})
]
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package.json
"scripts": {
"dll-build": "NODE_ENV=production webpack --config webpack.dll.config.js",
},
2
3
# 2、splitChunks 拆包
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
minSize: 0,
minChunks: 1,
priority: 10,
chunks: 'initial'
},
common: {
name: 'common',
test: /[\\/]src[\\/]/,
chunks: 'all',
minSize: 0,
minChunks: 2
}
}
}
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 六、骨架屏
用 css 提前占好位置,当资源加载完成即可填充,减少页面的回流与重绘,同时还能给用户最直接的反馈。
图中使用插件:react-placeholder (opens new window)
关于实现骨架屏还有很多种方案,用Puppeteer
服务端渲染的挺多的
使用 css 伪类:只要 css 就能实现的骨架屏方案 (opens new window)
等等
# 七、窗口化
原理:只加载当前窗口能显示的 DOM 元素,当视图变化时,删除隐藏的,添加要显示的 DOM 就可以保证页面上存在的 dom 元素数量永远不多,页面就不会卡顿
图中使用的插件:react-window (opens new window)
安装:npm i react-window
引入:import { FixedSizeList as List } from 'react-window';
使用:
const Row = ({ index, style }) => (
<div style={style}>Row {index}</div>
);
const Example = () => (
<List
height={150}
itemCount={1000}
itemSize={35}
width={300}
>
{Row}
</List>
);
2
3
4
5
6
7
8
9
10
11
12
13
14
# 八、缓存
# 1、http 缓存
# keep-alive
判断是否开启:看response headers
中有没有Connection: keep-alive
。开启以后,看network
的瀑布流中就没有 Initial connection
耗时了
nginx 设置 keep-alive(默认开启)
# 0 为关闭
#keepalive_timeout 0;
# 65s无连接 关闭
keepalive_timeout 65;
# 连接数,达到100断开
keepalive_requests 100;
2
3
4
5
6
# Cache-Control / Expires / Max-Age
设置资源是否缓存,以及缓存时间
# Etag / If-None-Match
资源唯一标识作对比,如果有变化,从服务器拉取资源。如果没变化则取缓存资源,状态码 304,也就是协商缓存
# Last-Modified / If-Modified-Since
通过对比时间的差异来觉得要不要从服务器获取资源
更多 HTTP 缓存参数可参考:使用 HTTP 缓存:Etag, Last-Modified 与 Cache-Control (opens new window)
# 2、Service Worker
借助 webpack 插件WorkboxWebpackPlugin
和ManifestPlugin
,加载 serviceWorker.js,通过serviceWorker.register()
注册
new WorkboxWebpackPlugin.GenerateSW({
clientsClaim: true,
exclude: [/\.map$/, /asset-manifest\.json$/],
importWorkboxFrom: 'cdn',
navigateFallback: paths.publicUrlOrPath + 'index.html',
navigateFallbackBlacklist: [
new RegExp('^/_'),
new RegExp('/[^/?]+\\.[^/]+$'),
],
}),
new ManifestPlugin({
fileName: 'asset-manifest.json',
publicPath: paths.publicUrlOrPath,
generate: (seed, files, entrypoints) => {
const manifestFiles = files.reduce((manifest, file) => {
manifest[file.name] = file.path;
return manifest;
}, seed);
const entrypointFiles = entrypoints.app.filter(
fileName => !fileName.endsWith('.map')
);
return {
files: manifestFiles,
entrypoints: entrypointFiles,
};
},
}),
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 九、预加载 && 懒加载
# 1、preload
就拿 demo 中的字体举例,正常情况下的加载顺序是这样的:
加入preload:
<link rel="preload" href="https://fonts.gstatic.com/s/longcang/v5/LYjAdGP8kkgoTec8zkRgqHAtXN-dRp6ohF_hzzTtOcBgYoCKmPpHHEBiM6LIGv3EnKLjtw.119.woff2" as="font" crossorigin="anonymous"/>
<link rel="preload" href="https://fonts.gstatic.com/s/longcang/v5/LYjAdGP8kkgoTec8zkRgqHAtXN-dRp6ohF_hzzTtOcBgYoCKmPpHHEBiM6LIGv3EnKLjtw.118.woff2" as="font" crossorigin="anonymous"/>
<link rel="preload" href="https://fonts.gstatic.com/s/longcang/v5/LYjAdGP8kkgoTec8zkRgqHAtXN-dRp6ohF_hzzTtOcBgYoCKmPpHHEBiM6LIGv3EnKLjtw.116.woff2" as="font" crossorigin="anonymous"/>
2
3
# 2、prefetch
场景:首页不需要这样的字体文件,下个页面需要:首页会以最低优先级 Lowest 来提前加载
加入prefetch:
<link rel="prefetch" href="https://fonts.gstatic.com/s/longcang/v5/LYjAdGP8kkgoTec8zkRgqHAtXN-dRp6ohF_hzzTtOcBgYoCKmPpHHEBiM6LIGv3EnKLjtw.113.woff2" as="font"/>
<link rel="prefetch" href="https://fonts.gstatic.com/s/longcang/v5/LYjAdGP8kkgoTec8zkRgqHAtXN-dRp6ohF_hzzTtOcBgYoCKmPpHHEBiM6LIGv3EnKLjtw.118.woff2" as="font"/>
<link rel="prefetch" href="https://fonts.gstatic.com/s/longcang/v5/LYjAdGP8kkgoTec8zkRgqHAtXN-dRp6ohF_hzzTtOcBgYoCKmPpHHEBiM6LIGv3EnKLjtw.117.woff2" as="font"/>
2
3
需要的页面,从prefetch cache
中取
webpack 也是支持这两个属性的:webpackPrefetch 和 webpackPreload (opens new window)
# 3、懒加载
# 图片
机械图片
渐进式图片(类似高斯模糊)
需要 UI 小姐姐出稿的时候指定这种格式
响应式图片
原生模式:<img src="./img/index.jpg" sizes="100vw" srcset="./img/dog.jpg 800w, ./img/index.jpg 1200w"/>
# 路由懒加载
通过函数 + import 实现
const Page404 = () => import(/* webpackChunkName: "error" */'@views/errorPage/404');
# 十、ssr && react-snap
- 服务端渲染 SSR,vue 使用 nuxt.js,react 使用 next.js
- react-snap 可以借助 Puppeteer 实现先渲染单页,然后保留 DOM,发送到客户端
# 十一、体验优化
# 白屏 loading
loading.html 需要自取哦,还有种方式,使用
webpack
插件HtmlWebpackPlugin
将 loading 资源插入到页面中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Loading</title>
<style>
body {
margin: 0;
}
#loadding {
position: fixed;
top: 0;
bottom: 0;
display: flex;
width: 100%;
align-items: center;
justify-content: center;
}
#loadding > span {
display: inline-block;
width: 8px;
height: 100%;
margin-right: 5px;
border-radius: 4px;
-webkit-animation: load 1.04s ease infinite;
animation: load 1.04s ease infinite;
}
@keyframes load {
0%,
100% {
height: 40px;
background: #98beff;
}
50% {
height: 60px;
margin-top: -20px;
background: #3e7fee;
}
}
</style>
</head>
<body>
<div id="loadding">
<span></span>
<span style="animation-delay: 0.13s"></span>
<span style="animation-delay: 0.26s"></span>
<span style="animation-delay: 0.39s"></span>
<span style="animation-delay: 0.52s"></span>
</div>
</body>
<script>
window.addEventListener("DOMContentLoaded", () => {
const $loadding = document.getElementById("loadding");
if (!$loadding) {
return;
}
$loadding.style.display = "none";
$loadding.parentNode.removeChild($loadding);
});
</script>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 招聘小广告
微医集团
医院支撑组- 坐标:浙江杭州
- 社招 3 年+ 或者 21 届实习
- 简历邮箱: wujj@wedoctor.com
# 更多性能优化,敬请期待~
学习 + 整理这些花了很长时间,路过的大哥们、小姐姐们还请给个赞 👍,有问题的也可留言交流哈 😝
# 参考文章:
- Mr.Max 慕课主页 (opens new window)
- 前端性能优化之雅虎 35 条军规 (opens new window)
- webpack 实践——webpack-bundle-analyzer 的使用 (opens new window)
- nginx 开启 gzip (opens new window)
- gzip 压缩文件&webPack 配置 Compression-webpack-plugin (opens new window)
- webpack 常用的三种 JS 压缩插件 (opens new window)
- 只要 css 就能实现的骨架屏方案 (opens new window)
- 使用 HTTP 缓存:Etag, Last-Modified 与 Cache-Control (opens new window)
- webpackPrefetch 和 webpackPreload (opens new window)
- 01
- 2023/07/03 00:00:00
- 02
- 2023/04/22 00:00:00
- 03
- 2023/02/16 00:00:00