聊一聊前端性能与体验的优化
前言
性能优化
,每个工程师跑不掉的一个话题。这里是本人总结的一些优化手法,希望对大家有所帮助,后续也会继续更新。演示源码和 PPT 无条件分享。
演示 PPT (一定要看,超帅)
横屏观看更佳:http://118.25.49.69:8086
前端性能的影响
前端性能的一个重要指标是页面加载时间,不仅事关用户体验,也是搜索引擎排名考虑的一个因素。
来自 Google 的数据表明,一个有 10 条数据 0.4 秒能加载完的页面,变成 30 条数据 0.9 秒加载完之后,流量和广告收入下降 90%
。Google Map 首页文件大小从 100KB
减小到70-80KB
后,流量在第一周涨了10%
,接下来的三周涨了25%
。亚马逊的数据表明:加载时间增加 100 毫秒
,销量就下降 1%
。
所以:重铸性能之光,我辈义不容辞😎
一、调试工具
磨刀不误砍柴工,读完大学再打工!
1.1 Network
这里可以看到资源加载详情,初步评估影响页面性能
的因素。鼠标右键可以自定义选项卡,页面底部是当前加载资源的一个概览。DOMContentLoaded
DOM 渲染完成的时间,Load
:当前页面所有资源加载完成的时间
思考:如何判断哪些资源对当前页面加载无用,做对应优化?
shift + cmd + P 调出控制台的扩展工具,添加规则
扩展工具 更多使用姿势
瀑布流 waterfall
Queueing
浏览器将资源放入队列时间Stalled
因放入队列时间而发生的停滞时间DNS Lookup
DNS 解析时间Initial connection
建立 HTTP 连接的时间SSL
浏览器与服务器建立安全性连接的时间TTFB
等待服务端返回数据的时间Content Download
浏览器下载资源的时间
1.2 Lighthouse
First Contentful Paint
首屏渲染时间,1s 以内绿色Speed Index
速度指数,4s 以内绿色Time to Interactive
到页面可交换的时间
根据 chrome 的一些策略自动对网站做一个质量评估,并且会给出一些优化的建议。
1.3 Peformance
对网站最专业的分析~后面会多次讲到
1.4 webPageTest
可以模拟不同场景下访问的情况,比如模拟不同浏览器、不同国家等等,在线测试地址:webPageTest (https://www.webpagetest.org/)
1.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"
开启 source-map
webpack.config.js
module.exports = {
mode: 'production',
devtool: 'hidden-source-map',
}
package.json
"analyze": "source-map-explorer 'build/*.js'",
npm run analyze
二、WEB API
工欲善其事,必先利其器。浏览器提供的一些分析 API至关重要
2.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);
其实有很多隐藏的 api,这里大家有兴趣的可以去试试看:
2.2 观察长任务(performance 中 Task)
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry)
}
})
observer.observe({entryTypes: ['longtask']})
2.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.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.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
三、老生常谈,雅虎军规
磨好刀了,就该想想往哪里捅比较好了~ 🗡🗡🗡
关于雅虎军规,你知道的有多少条,平时写用到的又有哪些?针对以下规则,我们可以做很多优化工作
3.1 减少 cookie 传输
cookie 传输会造成带宽浪费,可以:
减少 cookie 中存储的东西 静态资源不需要 cookie,可以采用其他的域名,不会主动带上 cookie。
3.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);
看下效果,很明显的卡顿
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);
再看下效果,很流畅~
performance
分析结果,load 事件之后也没有了那么多的红色标记
感兴趣的可以去了解一下 fastDom:github fastdom在线预览:fastdom demo (http://wilsonpage.github.io/fastdom/examples/animation.html)
关于任务拆分与组合的思想,react fiber
架构做的很牛逼,有兴趣的可以去了解一下调度算法在 fiber 中的实践
四、压缩
嗯哼哼、确定一下没有走错场子,继续继续!
4.1 Gzip
开启方式可参考:nginx 开启 gzip
还有一种方式:打包的时候生成 gz 文件,上传到服务器端,这样就不需要 nginx 来压缩了,可以降低服务器压力。可参考:gzip 压缩文件&webPack 配置 Compression-webpack-plugin
4.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}`);
});
package.json
"start": "npm run build && node server.js",
4.3 JavaScript、Css、Html 压缩
工程化项目中直接使用对应的插件即可,webpack 的主要有下面三个:
UglifyJS webpack-parallel-uglify-plugin terser-webpack-plugin 具体优缺点可参考:webpack 常用的三种 JS 压缩插件。 压缩原理
简单的讲就是去除一些空格、换行、注释,借助 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"
})
]
html 压缩可以用HtmlWebpackPlugin
,单页项目就一个 index.html,性能提升微乎其微~
4.4 http2 首部压缩
http2 的特点
二进制分帧 首部压缩 流量控制 多路复用 请求优先级 服务器推送 http2_push: 'xxx.jpg'
具体升级方式也很简单,修改一下 nginx 配置,方法请自行Google
五、Webpack 优化
上文中也提到了部分 webpack 插件,下面我再来看看还有哪些~
5.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")
})
]
};
package.json
"scripts": {
"dll-build": "NODE_ENV=production webpack --config webpack.dll.config.js",
},
5.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
}
}
}
},
六、骨架屏
用 css 提前占好位置,当资源加载完成即可填充,减少页面的回流与重绘,同时还能给用户最直接的反馈。图中使用插件:react-placeholder
关于实现骨架屏还有很多种方案,用Puppeteer
服务端渲染的挺多的
使用 css 伪类:只要 css 就能实现的骨架屏方案
等等
七、窗口化
原理:只加载当前窗口能显示的 DOM 元素,当视图变化时,删除隐藏的,添加要显示的 DOM 就可以保证页面上存在的 dom 元素数量永远不多,页面就不会卡顿
图中使用的插件:react-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>
);
八、缓存
8.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;
Cache-Control / Expires / Max-Age
设置资源是否缓存,以及缓存时间
Etag / If-None-Match
资源唯一标识作对比,如果有变化,从服务器拉取资源。如果没变化则取缓存资源,状态码 304,也就是协商缓存
Last-Modified / If-Modified-Since
通过对比时间的差异来觉得要不要从服务器获取资源
更多 HTTP 缓存参数可参考:使用 HTTP 缓存:Etag, Last-Modified 与 Cache-Control
8.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,
};
},
}),
九、预加载 && 懒加载
9.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"/>
9.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"/>
需要的页面,从prefetch cache
中取
webpack 也是支持这两个属性的:webpackPrefetch 和 webpackPreload
9.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
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>
参考文章
前端性能优化之雅虎 35 条军规 (https://juejin.cn/post/6844903657318645767#heading-1) webpack 实践——webpack-bundle-analyzer 的使用 (https://segmentfault.com/a/1190000012220132) nginx 开启 gzip](https://juejin.cn/post/6844903605187641357) gzip 压缩文件&webPack 配置 Compression-webpack-plugin (https://segmentfault.com/a/1190000020976930) webpack 常用的三种 JS 压缩插件 (https://blog.csdn.net/qq_24147051/article/details/103557728) 只要 css 就能实现的骨架屏方案 (https://segmentfault.com/a/1190000020437426) 使用 HTTP 缓存:Etag, Last-Modified 与 Cache-Control (https://harttle.land/2017/04/04/using-http-cache.html) webpackPrefetch 和 webpackPreload (https://www.cnblogs.com/skychx/p/webpack-webpackChunkName-webpackPreload-webpackPreload.html)
最后
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)
欢迎加我微信「 sherlocked_93 」拉你进技术群,长期交流学习...
关注公众号「前端下午茶」,持续为你推送精选好文,也可以加我为好友,随时聊骚。