性能优化指标与测量工具
行业标准
- 理解加载瀑布图
横向解读具体资源加载:鼠标浮到某资源的瀑布图上出现以下浮框
Queueing:资源排队消耗时间。
DNS Lookup:域名解析所耗时间,请求某域名下的资源,浏览器需要先通过DNS解析器得到该域名服务器的IP地址。
Initial Connection / Connecting: 建立连接的时间花费,包含了TCP握手及重试时间。
SSL :ssl证书加密协商消时间。
Request sent:发送请求所消耗的时间。
Waiting (TTFB):请求发出到响应回来所经历的时间,这是最影响用户感受的一个参数。影响这个参数的最大两个因素是 ① 服务器端的处理能力 ② 网络的因素
Content Download:资源下载所消耗的时间。
纵向解读瀑布图
纵向主要注意两点:① 资源与资源主键的联系,如果发生阻塞资源与资源之间就是串行的,如果是并行的那么我们就能加快资源的加载 ② 查看资源加载的关键节点,蓝色线:DOM完全加载并解析完毕时间 红色:页面上所有DOM、CSS、JS、图片完全加载完毕时间。
- 基于HDR存储性能测试结果方便后续使用,或使用其他性能分析工具继续分析
- 使用Lighthouse进行分析
我们重点关注两个指标
First Contentful Paint:第一个有内容的绘制出现的时间,可以是文字也可以是图片,只要屏幕不在是白屏了。
Speed Index(速度指数):行业标准是4秒
- 交互的体验
① 交互动作的反馈时间 – 足够的快
② 帧率FPS – 画面动画足够流畅,标准1秒钟60帧
如何查看打开调试工具 Ctrl + shift + P
③ 异步请求完成时间 – 足够快,希望你优化所有的异步请求可以在1s之内把数据返回回来,如果返回不回来数据做压缩,如果压缩后时间仍然达不到,那么考虑前端交互上进行优化,例如加loading状态。
优化模型
RAIL测量模型 – google
可以对我们优化的结果进行量化,告诉我们做到多好才是好。
- R: Response响应(处理事件应在50ms以内完成) – 指的是对用户而言,点击页面元素,输入内容后网站是否及时给用户反馈
- A: Animation动画(每10ms产生一帧) – 动画在用户看到的时候是否足够流畅
- I: Idle空闲(尽可能增加空闲时间) – 我们要让浏览器有足够的空闲时间,比如说我们在浏览网页是突然卡死了,那么说明浏览器的主线程非常的忙碌,没有时间来处理你现在所做的事情。
- L: Load加载(在5s内完成内容加载并可以产生交互) – 指的是网络资源加载的时间
测量工具
- Chrome DevTools 开发调试、性能测评
- Lighthouse 网站整体质量评估
- WebPageTest 多测试地点、全面性能报告 – webpagetest.org/ 也可进行本地部署
性能相关APIs
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
// 计算一些关键的性能指标
window.addEventListener('load', (event) => {
// Time to Interactive 可交互时间
let timing = performance.getEntriesByType('navigation')[0];
console.log(timing.domInteractive);
console.log(timing.fetchStart);
let diff = timing.domInteractive - timing.fetchStart;
console.log("TTI: " + diff);
})
// 观察长任务
// 通过PerformanceObserver得到所以的long tasks对象
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry)
}
})
// 监听long tasks
observer.observe({entryTypes: ['longtask']})
// 可见性的状态监听(我们可以根据用户是否在使用页面来做一些事情,例如暂停视频、游戏存档...)
let vEvent = 'visibilitychange';
if (document.webkitHidden != undefined) {
// webkit 事件名称
vEvent = 'webkitvisibilitychange';
}
function visibilityChanged() {
// 页面可见
if (document.hidden || document.webkitHidden) {
console.log("Web page is hidden.")
} else { // 页面不可见
console.log("Web page is visible.")
}
}
document.addEventListener(vEvent, visibilityChanged, false);
// 判断用户当前网络状态(可根据用户网络加载不同清晰度的图片等等)
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);
复制代码
渲染优化
现代浏览器渲染原理
URL解析阶段 –> DNS域名解析 –> 建立TCP连接 –> 发送HTTP请求 –> HTTP响应阶段 –> 浏览器渲染
浏览器是多线程的,页面渲染是单线的;
① 拿到index.html后浏览器会开辟一块栈内存,同时分配一个主线程“自上而下,自左而右”的去解析执行;
② 在解析执行DOM结构时遇到link、img、video…浏览器会开启一个全新线程去加载资源,但不会执行。主线程继续向下执行;
- 遇到 style 内嵌样式正常解析执行
- @import 导入样式 (同步)不会开辟新线程去加载资源,而是主线程去获取资源并加载完毕才会继续向下继续解析执行DOM
③ 遇到script、外链js会去获取资源并执行,会阻塞DOM生成;优化:使用defer、async去异步获取资源,等dom渲染完执行;
- 现代浏览器都有完善的代码扫描机制,如果遇到script需要同步加载执行时,同时会向下扫描代码,如果发现有一些异步获取资源代码,此时就开始请求资源了
- 自我等待机制:因为在JS中可能会有操作元素样式的情况出现,所以哪怕异步请求的js资源加载完成了,执行js代码也会等到css加载并渲染完成后才会执行
- defer 异步获取资源后,按照顺序去执行
- async 异步获取资源后,执行是无序的,谁先加载回来谁先执行
- DOMContentLoaded() : 当DOM结构加载完成就会触发
- $(function(){})||$(doucment).ready(function(){})
- load() : 所有资源加载完成才会执行,包含图片等资源加载
④ 自上而下执行完毕后,会生成 DOM Tree
⑤ 主线程现在可以去执行加载回来的css资源了,执行完css会生成 CSSOM
⑥ 将 DOM Tree 和 CSSOM 结合生成 Render Tree(渲染树)
⑦ 浏览器通知GPU(显卡)开始按照Render Tree绘制图形到页面中
复制代码
可优化的渲染环节和方法
关键渲染路径
- 布局和绘制 – 这个两个步骤是我我们关键渲染路径用开销最大的两个步骤,如何避免这两个步骤是一个很好的优化点。
渲染树只包含网页需要的节点,布局计算每个节点精确的位置和大小,绘制是像素化每个节点的过程
重绘:元素样式的改变(但宽高、大小、位置等不变)
回流:元素的大小或者位置发生了变化,触发重新布局导致渲染树重新计算布局和渲染
重绘不一定回流,回流一定触发重绘
复制代码
- 避免回流
会导致回流的操作:
* 页面首次渲染
* 浏览器窗口大小发生改变
* 元素尺寸或位置发生改变元素内容变化(文字数量或图片大小等等)
* 元素字体大小变化
* 添加或者删除可见的 DOM 元素
* 激活 CSS 伪类(例如:hover)
* 查询某些属性或调用某些方法
* 一些常用且会导致回流的属性和方法
clientWidth、clientHeight、clientTop、clientLeftoffsetWidth、offsetHeight、offsetTop、offsetLeftscrollWidth、scrollHeight、scrollTop、scrollLeftscrollIntoView()、scrollIntoViewIfNeeded()、getComputedStyle()、
getBoundingClientRect()、scrollTo()
① 改变元素的位置,不要使用top、bottom等,改为使用transform: translate 做位移,这样就不会触发回流和重绘,只会触发复合的过程。
② 读写分离 - 读(读取布局信息)写(改变布局)批量读,批量写; 如果读写不进行分离的会造成页面的抖动。
插件: https://github.com/wilsonpage/fastdom
复制代码
- 复合线程和图层
复合线程做什么?
将页面拆分图层进行绘制再进行复合
页面怎样拆成不同图层的,规则是什么?
默认情况下由浏览器决定,浏览器根据一些规则决定是否要把页面拆分成不同图层,又把哪些元素拆成分不同图层,主要分析的是元素与元素之间是否有相互的影响,如果某些元素对其他元素影响比较多,那么它就会被提取为单独的图层,好处:如果它发生变化的化,我们只对它的图层进行重绘。或者我们可以使用定位|浮动 + z-index手动提取影响大的元素为单独一个图层;
利用DevTools了解网页的图层拆分情况;
如何查看打开调试工具 Ctrl + shift + P 搜 Layers
哪些样式仅影响复合;
Position transform: translate(npx,npx);
Scale transform: tscale(n);
Rotation transform: rotate(ndeg);
Opacity opacity: 0....1;
复制代码
代码优化
JavaScript优化
- JavaScript 的开销和如何缩短解析时间?
开销在哪里?
加载、解析&编译、执行
解决方案: Code splitting 代码拆分,按需加载;Tree shaking 代码减重;避免长任务;避免超过 1KB 的行间脚本;
复制代码
-高频事件处理函数 防抖
// 如果短时间内大量触发同一事件,只会执行一次函数
/*
* fn [function] 需要防抖的函数
* delay [number] 毫秒,防抖期限值
*/
function debounce(fn,delay){
let timer = null //借助闭包
return function() {
if(timer){
clearTimeout(timer) //进入该分支语句,说明当前正在一个计时过程中,并且又触发了相同事件。所以要取消当前的计时,重新开始计时
timer = setTimeout(fn,delay)
}else{
timer = setTimeout(fn,delay) // 进入该分支说明当前并没有在计时,那么就开始一个计时
}
}
}
复制代码
- 对象优化
以相同顺序初始化对象成员,避免隐藏类的调整;
V8引擎在解析的时候会创建隐藏的类型,可复用的类型会进行复用;
class RectArea { // HC0
constructor(l, w) {
this.l = l; // HC1
this.w = w; // HC2
}
}
const rect1 = new RectArea(3,4); // 创建了隐藏类HC0, HC1, HC2
const rect2 = new RectArea(5,6); // 相同的对象结构,可复用之前的所有隐藏类
const car1 = {color: 'red'}; // HC0
car1.seats = 4; // HC1
const car2 = {seats: 2}; // 没有可复用的隐藏类,创建HC2
car2.color = 'blue'; // 没有可复用的隐藏类,创建HC3
复制代码
实例化后避免添加新属性;
const car1 = {color: 'red'}; // In-object 属性
car1.seats = 4; // Normal/Fast 属性,存储在property store里,查找的时候需要通过描述数组间接查找,没有对象本身属性查找的快
复制代码
尽量使用Array代替 array-like (伪数组l例如:Arguments) 对象;
Array.prototype.forEach.call(arrObj, (value, index) => { // 不如在真实数组上效率高
console.log(`${ index }: ${ value }`);
});
const arr = Array.prototype.slice.call(arrObj, 0); // 转换的代价比影响优化小
arr.forEach((value, index) => {
console.log(`${ index }: ${ value }`);
});
复制代码
避免读取超过数组的长度
function foo(array) {
for (let i = 0; i <= array.length; i++) { // 越界比较
if(array[i] > 1000) { // 1.沿原型链的查找 2.造成undefined与数进行比较
console.log(array[i]); // 业务上无效、出错
}
}
}
复制代码
避免元素类型转换
// 如果数组中刚开始全是整型,v8对于这个做了优化,而你push了一个double的类型,那么v8做的优化先前做的优化就无效了,需要降级优化。你如果类型越具体,编译器能做的优化就越多;
const array = [3, 2, 1]; // PACKED_SMI_ELEMENTS
array.push(4.4); // PACKED_DOUBLE_ELEMENTS
复制代码
- 尽量减少闭包使用,避免嵌套循环和死循环
HTML优化
- 减少iframes使用
原因:iframes会阻塞父文档的加载,在iframes中创建的元素相较于在父文档上创建同样的元素开销要大的多。
非要使用解决方案:延迟加载
<iframes id='a'></iframes>
doucment.getElementById('a').setAttribute('ser', 'url')
复制代码
- 压缩空白符
- 避免节点深层次嵌套
- 避免使用table布局
- 删除注释
- css&js尽量使用外链
- 删除元素默认属性
可借助工具 – html-minifier
CSS优化
- CSS解析规则从右向左,所以尽可能减少层级嵌套
- 降低css对渲染的阻塞
把css样式表置于顶部放在head中;减小css体积;
- 利用GPU进行完成动画
- 使用contain属性
contain: layout;表示元素外部无法影响元素内部的布局,反之亦然;contain属性允许开发者声明当前元素和它的内容尽可能的独立于 DOM 树的其他部分。这使得浏览器在重新计算布局、样式、绘图、大小或这四项的组合时,只影响到有限的 DOM 区域,而不是整个页面,可以有效改善性能;
- 使用font-display属性
它会帮助我们让文字更早的显示在页面上,还会适当的减轻文字闪动的问题。
资源优化
压缩&合并
- HTML压缩
使用在线工具进行压缩kangax.github.io/html-minifi… 使用html-minifier等npm工具;
- CSS压缩
使用在线工具进行压缩kangax.github.io/html-minifi… 使用clean-css等npm工具;
- js压缩与混淆 – webpack对js构建时压缩
- css js 文件合并
若干小文件合并,OK!无冲突,服务相同的模块,OK!单纯为了优化网络加载,No!
图片格式
- JPEG/JPG
优点:很高的压缩比,但是我们的画质还可以很好的被保存。
使用场景:当你需要展示比较大的图片还想保存展现这样画质效果的时候,比如首页轮播入。
缺陷:如果图片存在很强的纹理和边缘,那么它会存在锯齿感模糊。
工具:https://github.com/imagemin/imagemin 生成或者压缩jpg图片
复制代码
- PNG
优点:可以做透明背景图片
使用场景:对JPG做的一些弥补,可以对线条、纹理、边缘有很好的展示
缺陷:体积相较大一点,我们会用来做一些小的图片例如logo、图标
工具:https://github.com/imagemin/imagemin-pngquant 生成或者压缩png图片
复制代码
- webp
优点:可以和png一样有同样的质量,但是能还能比png压缩比高
缺点:还得看一下浏览器兼容
复制代码
- 图片base64
BASE64的代码很多,不方便开发维护(慎用):基于webpack的相关加载器-file-loader可以自动把一些图片BASE64
复制代码
图片加载
- 图片的懒加载(lazy loading)
① 原生的图片懒加载方法 - <img loading="lazy" src="https://xxxxx" > loading="lazy"只需要浏览器支持这个属性即可
② 第三方懒加载方案 - verlok/lazyload yall.js Blazy
复制代码
- 使用渐进式图片
jpg有两种基本的格式:① 基线jpg,加载时自上而下行扫描的形式 ② 渐进式格式,那么它会从低像素到高像素加载的过程。
复制代码
渐进式图片工具:progressive-image ImageMagick libjpeg jpegtran jpeg-recompress imagemin
- 使用响应式图片
① Srcset属性的使用
② Sizes属性的使用
③ picture元素的使用
复制代码
字体优化
问题:字体未下载完成后时,浏览器隐藏或者自动降级,导致字体闪烁,Flash of invisible text (文字从看不到到可以看见,造成字体闪烁问题) Flash of unstyled text (文字开始是默认的一种样式,字体下载完又是另一种样式,造成字体闪烁问题) 这两种问题是不可避免的,因为字体要通过网络下载需要一定的时间,只要它没有下载完成那么浏览器就必须做做一个选择,要么一直看不见等下载完成在显示,要么先使用默认字体等下载完成后再显示另一种字体。
- 使用 font-display 控制浏览器的这个行为
font-display: auto | block | swap | fallback | optional;
- 使用 unicode-range属性做字符集拆分
- 使用ajax + base64加载字体
构建优化
依赖优化
- noParse
提高构建速度,直接通知webpack忽略较大的库,被忽略的库不能有import,require,define的引入方法
module: {
noParse: /lodash/,
}
复制代码
- DllPlugin
避免打包时对不变的库重复构建,提高构建速度。一般应用场景为开发环境
代码拆分
- 手工定义入口
- 使用webpack的splitChunks提取公有代码,拆分业务代码
代码压缩 – 基于webpack4的资源压缩(minification)
- Terser压缩js
- mini-css-exteact-plugin 压缩css
- HtmlWebpackPlugin-minify 压缩html
持久化缓存 – 基于webpack的资源持久化缓存
- 每个打包的资源文件有唯一的hash值
- 修改后只有受影响的文件hash变化
监测与分析 – 基于webpack的应用大小监测与分析
- Stats 分析与可视化图
https://alexkuz.github.io/webpack-chart/
进一步分析: npm i source-map-explorer
添加命令分析
"scripts": {
"analyze": "source-map-explorer 'build/&.js'"
}
复制代码
- webpack-bundle-analyzer 进行体积分析
- speed-measure-webpack-plugin 速度分析
按需加载
- 组件动态加载
- 路由动态加载
传输加载优化
GZip
# nginx开启gzip
# 开启gzip
gzip on;
# 启用gzip压缩的最小文件;小于设置值的文件将不会被压缩
gzip_min_length 1k;
# gzip 压缩级别 1-10
gzip_comp_level 2;
# 进行压缩的文件类型。
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
# 是否在http header中添加Vary: Accept-Encoding,建议开启
gzip_vary on;
# 对gzip已经压缩过的静态资源直接利用
gzip_static on;
# 以16k为单位,按照原始数据大小以16k为单位的4倍申请内存
gzip_buffers 4 16k;
# 使用的http版本
gzip_http_version 1.1;
复制代码
KeepAlive
帮助我们对tcp连接进行复用,当我们和一台服务器建议了tcp链接之后,接下来的请求就不需要重复的建立连接了。http标准的一部分,在http1.1之后,KeepAlive这个参数是默认开启的。
# nginx配置
# 超时的时间,当我们客户端和服务器建立了tcp链接后,服务器会保持tcp链接,当65s后没有使用,那么就会关闭
keepalive_timeout 65;
# 利用这tcp链接一共可以发送100个请求,超过后重新建立链接
keepalive_requests 100;
复制代码
HTTP缓存
developer.mozilla.org/zh-CN/docs/…
缓存位置
Service Worker: 浏览器独立线程进行缓存
Memory Cache: 内存缓存
Disk Cache: 硬盘缓存
Push Cache: 推送缓存(HTTP/2中的)
复制代码
- 协商缓存 – 有2种:ETag/if-None-Match 和 Last-Modified/if-Modify-Since
**协商缓存原理:** 客户端向服务器端发出请求,服务端会检测是否有对应的标识,如果没有对应的标识,服务器端会返回一个对应的标识给客户端,客户端下次再次请求的时候,把该标识带过去,然后服务器端会验证该标识,如果验证通过了,则会响应304,告诉浏览器读取缓存。如果标识没有通过,则返回请求的资源。(nginx默认开启这些配置)
复制代码
- 强缓存 – Expres和Cache-Control
# nginx配置
location / {
if($request_filename ~* .*\.(?:htm|html)$)
{
add_header Cache-Control " no-cache, must-revalidate";
add_header "Pragma" "no-cache";
add_header "expires" "0";
}
if($request_filename ~* .*\.(?:js|css)$)
{
# 7天
expires 7d;
}
if($request_filename ~* *\.(?:gif|jpg|jpeg|png|bmp|swf|ico|cur|gz|svg)$)
{
expires 7d;
}
index index.html index.htm;
}
复制代码
Service Worker 缓存
① 加速重复访问 ② 离线支持
Service Worker如何实现,vue和react中都有具体的实现方法,自行查找资料。
HTTP/2
优势:① 二进制传输 ② 请求响应多路复用 ③ Server push
什么是多路复用?
在 HTTP 1.1 中,发起一个请求是这样的:
浏览器请求 url -> 解析域名 -> 建立 HTTP 连接 -> 服务器处理文件 -> 返回数据 -> 浏览器解析、渲染文件
这个流程最大的问题是,每次请求都需要建立一次 HTTP 连接,也就是我们常说的3次握手4次挥手,这个过程在一次请求过程中占用了相当长的时间,而且逻辑上是非必需的,因为不间断的请求数据,第一次建立连接是正常的,以后就占用这个通道,下载其他文件,这样效率多高啊!
为了解决这个问题, HTTP 1.1 中提供了 Keep-Alive,允许我们建立一次 HTTP 连接,来返回多次请求数据。
但是这里有两个问题:
HTTP 1.1 基于串行文件传输数据,因此这些请求必须是有序的,所以实际上我们只是节省了建立连接的时间,而获取数据的时间并没有减少
最大并发数问题,假设我们在 Apache 中设置了最大并发数 300,而因为浏览器本身的限制,最大请求数为 6,那么服务器能承载的最高并发数是 50
而 HTTP/2 引入二进制数据帧和流的概念,其中帧对数据进行顺序标识,这样浏览器收到数据之后,就可以按照序列对数据进行合并,而不会出现合并后数据错乱的情况。同样是因为有了序列,服务器就可以并行的传输数据。
HTTP/2 对同一域名下所有请求都是基于流,也就是说同一域名不管访问多少文件,也只建立一路连接。同样Apache的最大连接数为300,因为有了这个新特性,最大的并发就可以提升到300,比原来提升了6倍。
复制代码
SSR
加速首屏加载;更好的seo;
- nuxt
- next
使用CDN资源
在客户端和服务器进行信息交互时,对于多项数据尽量基于JSON格式进行传输
更多优化技术
DNS预获取
在我们网站里面可能会存在很多域名,但是每次请求都进行DNS解析时会浪费一定的时间;所以我们可以做一下优化:DNS预获取(在head先获取)
SVG优化图标
优势:色彩更加丰富,具有语义化,独立的矢量图。
FlexBox布局
优势:更高性能的实现方案;容器有能力决定子元素的大小,顺序,对齐,间隔;双向布局;
优化资源的加载顺序
- Preload: 提前加载较晚出现,但是对当前页面非常重要的资源
- Prefetch: 提前加载后续页面需要的资源,优先级低
预渲染
大型单页 应用的性能瓶颈: js下载 + 解析 + 执行
SSR的主要问题: 牺牲TTFB来补救first Paint;实现复杂;
Pre-rendering 打包时提前渲染页面,没有服务端参与;
- react-snap – react和vue都可使用
Windowing(窗口化)提高列表性能 -虚拟列表
窗口化 的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom 节点的时间
- vue-virtual-scroll-list
- react-window
骨架组件
占位减少首页空白,减少布局移动。提升用户感知。
- react-placeholder
- vue-skeleton-webpack-plugin