本文根据这篇文章:Best Practices for Speeding Up Your Web Site,被称作”雅虎35条军规”,虽里面有的东西虽说也已经过时,但可以由此窥探前端发展的历程,一些过时的建议会略去解释。
一道面试题引发的前端性能优化思考
相信大家都碰过这道经典面试题:在浏览器输入url到看到页面的展示,这中间发生了什么?
总体答案是这样:
- 浏览器输入域名(Domain) ——> DNS服务器解析成IP地址 (如:12.220.12.123)
- 通过局域网 ——> 交换机 ——> 路由器 ——> 主干网 ——> 服务端
- 建立TCP连接
- 服务器接收请求,查库,读文件等, 拼接好response,即返回的HTTP响应
- 浏览器收到首屏html页面,开始渲染
- 解析 html 为 DOM-tree
- 解析 CSS 为 CSS-tree
- DOM-tree + CSS-tree 生成 render-tree 绘图
- 根据页面节点的改变,样式的改变等,发生重绘与回流
所谓的性能优化,就是上面的步骤加在一起,时间尽可能短,所以在这个过程种3点关键的优化因素:
- 减少http请求,缩小http请求大小
- 减少静态文件大小
- 减少渲染
大概优化点,实际工作种还要结合场景才能做出优化:
- DNS 通过缓存减少DNS查询时间
- 网络请求过程走最近的网络环境
- 相同静态资源是否可缓存
- 减小http请求大小
- 减少http请求
- 服务的渲染
网页性能检测工具
首先应明确检测网页所需的工具,这里列举了3种方法:
谷歌推出的性能检测网站web.dev
从三个指标对网站性能进行评估
- Largest Contentful Paint (LCP) :最大内容绘制,测量加载性能。为了提供良好的用户体验,LCP 应在页面首次开始加载后的2.5 秒内发生。
- First Input Delay (FID) :首次输入延迟,测量交互性。为了提供良好的用户体验,页面的 FID 应为100 毫秒或更短。
- Cumulative Layout Shift (CLS) :累积布局偏移,测量视觉稳定性。为了提供良好的用户体验,页面的 CLS 应保持在 0.1. 或更少。
该网站可以输入网址后进行测试,给出一份详细的 lighthouse
报告
Navigator.sendBeacon() 埋点传输数据给服务器记录时间
依据google以上3原则,可以在前端进行埋点,进行统计
1 | import { getCLS, getFID, getLCP } from 'web-vitals' |
window.performance
打开任意网页,控制台输入以下代码,
可以看到 window.performance
所获取到的东西,我们主要看他的timing
属性,用开始时间减去结束时间得出各种资源加载的时间。以下的数值都算是比较粗略的数值
1 | const t = window.performance.timing; |
第一字节响应时间(TTFB)= 从发送请求到WEB服务器的时间 + WEB服务器处理请求并生成响应花费的时间 + WEB服务器生成响应到浏览器花费的时间
第一字节响应时间(TTFB)要考虑的问题:
步骤1:从发送请求到WEB服务器的时,即向站点地址提交首次请求
- DNS 响应时间(终端用户侧解析 DNS 请求有多块)
- 网站服务器到终端用户的距离,越短越好
- 网络稳定性
步骤2:WEB服务器处理请求并生成响应花费的时间,即由 web 服务器解析本次请求
- 物理硬件响应时间 (web 服务器解析请求有多快)
- 既有的服务器操作负载
- 数据中心任何网络相关的延迟
步骤3:WEB服务器生成响应到浏览器花费的时间, 即向客户端发送首个响应的时间
- 终端用户的网速
- 连接稳定性
Chrome自带的performance检测工具
自动化检测利器—— lighthouse
—— 自动生成网页性能报告,并有优化建议
符合谷歌的 PWA 标准的检测,关于PWA的概念,点击这里
方法一,直接安装 npm 库
1 | npm i -g lighthouse |
使用起来超级简单
1 | lighthouse <需要测试的网页链接> |
方法二,浏览器也自带 lighthouse
功能,以下是 Edge
浏览器
浏览器的重绘与回流
资源合并与压缩:
现代化的前端跑不掉资源压缩这一步,平时工程化用的 webpack
就是一款压缩工具
HTML压缩
原理:将HTML里的空格,换行符,制表符等去掉
方法:
- node作为构建工具,提供了 html-minifier 工具,webpack的 HTMLMinifierWebpackPlugin 中内置了该构建工具
- 后端模板引擎渲染压缩,如ejs模板,
express
的renderFile()
CSS压缩
原理:
- 除了像HTML一样删除空格,换行符等之外
- 删除无效代码
- css语义合并
方法:
- html-minifier 可以对html中的内联css样式进行压缩,需配置其中的选项
- clean-css 对 css 的压缩
- Webpack 的 CssMinimizerWebpackPlugin
JS压缩与混乱
原理:
- 删除无效字符
- 删除注释
- 代码语义化的缩减和优化
- 代码保护
方法:
- html-minifier 可以对html中的js进行压缩,需配置其中的选项
- uglifyjs3
JS文件合并
如果不合并请求会有以下影响
但合并请求也不是万能的,其缺点体现在:
首屏渲染的问题
现在的前端都用类似react或者vue等前端框架,如果使用vue或者react没有进行服务端渲染的操作,而且服务端合并请求的JS文件又比较大,那么请求回来的JS加载完成后,才会执行react或vue的框架代码在客户端加载渲染,那么首屏白屏的时间就会比较久缓存失效的问题
现在的打包用webpack都会加上md5戳,用于标识单个js文件是否发生改变,如果JS合并了,就会使原先的md5戳失效,就得重新加载
针对合并带来的负面效果,应遵循以下原则:
公共库合并
公共库代码比较少做频繁变动,应单独打包为一个js文件,和业务代码的js文件分开,避免公共库的缓存失效不同页面的合并
这种发生在vue或react的单页面应用,通常我们打包出来的单页应用只有一个js文件,但这种方法在效率提升来说有阻碍,最好的方法是每个页面打包为一个js文件,当某页面被路由到加载到时,才去加载对应页面的js文件,这种方式在webpack中有相应的解决方案,就是loadable
异步加载组件
传输方面:
CDN缓存
http2
充分利用HTTP缓存
压缩html,js,css文件体积,用gzip/brotli
对 JS、CSS、HTML 等文本资源均有效,但是对图片效果不大。
- gzip 通过 LZ77 算法与 Huffman 编码来压缩文件,重复度越高的文件可压缩的空间就越大。
- brotli 通过变种的 LZ77 算法、Huffman 编码及二阶文本建模来压缩文件,更先进的压缩算法,比 gzip 有更高的性能及压缩率
可在浏览器的 Content-Encoding
响应头查看该网站是否开启了压缩算法,目前知乎、掘金等已全面开启了 brotli
压缩。
1 | # Request Header |
压缩混淆工具
-
JS压缩
上述提到的
gzip/brotli
和terser
webpack
打包-
webpack-bundle-ananlyze
分析打包体积 - 使用一些更小体积的库,如 moment -> dayjs
- 一些库进行按需加载,如 import lodash -> import lodash-es
-
雅虎35条的链接, 从中挑出一些点来讲,如下:
Image *
之所以把图片放在最前,因为优化图片的效率是比较大的
Optimize Images * 图片优化
各种图片格式的应用场景:
1.1 png 大部分需要使用透明的场景
pngcrush 或其他工具压缩png。在线压缩工具 https:/ tinypng.com/1.2 jpg 大部分不需要透明的场景
jpegtran或其它工具压缩jpeg,大图用jpg1.3 SVG矢量图,类似XML语法,内嵌在html里的代码图片(用来绘制地图,股票K线图等),可使用 阿里的iconfont 解决icon问题
SVG的教程例如画一个长方形:
1
2
3
4
5
6
7<svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect width="300" height="100"
style="fill:rgb(0,0,255);stroke-width:1;
stroke:rgb(0,0,0)" />
</svg>1.4 Webp 可全用
google在2010年开发的一种全能的图片,但safari浏览器 和 webview 的兼容性不太好1.5 png ——> Webp 格式网站:智图
webp 比 jpeg/png 更小,而 avif 又比 webp 小一个级别
为了无缝兼容,可选择 picture/source 进行回退处理
1 | <picture> |
- Image inline,即将图片内容内嵌到html里,减少网站的HTTP请求数量,多用于移动端和小图标:
如何将图片转换为base64编码内嵌到html中,有一个在线转换网站
,在html文件图片所在的src=””中添加data:image/jpg;base64,(注:这里是jpg格式,你可以改写成你编码图片的类型)
,将你编码的Base64代码复制到image/jpg;base64, 的后面,然后用浏览器运行即可。
当然还可以像taobao主页一样,嵌入到css的属性中使用:
Optimize CSS Sprites
实际上,随着带宽的普遍提高,现在不少网站都不用雪碧图了,特别是PC端,移动端用的还相对比PC端多些。但仍有网站在使用他。而雪碧图在应用上也有缺点,如果过多图标集合在一个图的话,则会出现如果该图片请求后读取不出,所有应用到该图片的图标全部会失效
- Arranging the images in the sprite horizontally as opposed to vertically usually results in a smaller file size.
- Combining similar colors in a sprite helps you keep the color count low, ideally under 256 colors so to fit in a PNG8.
- “Be mobile-friendly” and don’t leave big gaps between the images in a sprite. This doesn’t affect the file size as much but requires less memory for the user agent to decompress the image into a pixel map. 100x100 image is 10 thousand pixels, where 1000x1000 is 1 million pixels
雪碧图最好竖放,避免横放,达到最小尺寸
相似图片合并,颜色相近的合并,颜色数会更少
移动端的雪碧图减少空隙
雪碧图的生成,用这个网站
选择器性能
内联样式 (style=””) > ID 选择器 (id) > 类选择器 (class) = 属性选择器 ( a[href], input[type=”text”] 等 ) = 伪类选择器 (nth-child(n), :hover, :active 等) > 元素(类型)选择器 = 伪元素选择器
Do Not Scale Images in HTML
If you need
<img width="100" height="100" src="mycat.jpg" alt="My Cat" />
then your image (mycat.jpg) should be 100x100px rather than a scaled down 500x500px image.
不要在HTML中缩放图片,如果你需要 <img width="100" height="100" src="mycat.jpg" alt="My Cat" />
的图片,直接做一张 100*100
的图即可,而不是拿一张 500*500
的图片进行缩放
Make favicon.ico Small and Cacheable
使得 favicon.ico
图片可以缓存,如果不进行缓存,不关心他,浏览器还是会请求他
Content
Make Fewer HTTP Requests *
减少 HTTP 请求
Reduce DNS Lookups
减少DNS查询
Avoid Redirects *
避免重定向
Postload Components * 懒加载组件
图片进入可视区域后再请求图片资源。适用于电商等图片很多,页面很长的业务场景
减少无效资源的加载
并发加载的资源过多会阻塞js的加载,影响网站正常使用
案例可以看我的另一篇文章,关于JS IntersectionObserver Api,可以这个将这个api用于懒加载
下面结合react
写一个懒加载案例
1 | import React from 'react' |
Preload Components * 预加载组件
方法一,加载时另 display
为 none
1 | <img src="http://xxx.xxx" style="display: none" /> |
方法二,利用 new Image()
1 | var img = new Image() |
方法三,XHLHttpRequest
对象,这种方法有跨域问题
1 | var xmlhttprequest = new XMLHttpRequest() |
方法四,使用库 PreloadJS
进行预加载
1 | var queue = new createjs.LoadQueue(false) |
Server 服务端优化
Use a Content Delivery Network (CDN) *
CDN,内容分发网络,用于静态资源的加载,选择离用户近的服务器节点,节省物理上的距离,让资源到达用户的物理距离缩短
那么请求CDN内容时,如果请求头携带cookie是没用的,所以请求CDN时,cookie最好去掉,CDN的域名如果和主站域名一样,就会携带Cookie过去,所以找CDN时最好不要和主站的域名一样
例如淘宝的页面,直接在头部加DNS script标签即可。
1 | <link rel="dns-prefetch" href="//g.alicdn.com" /> |
Add Expires or Cache-Control Header *
There are two aspects to this rule:
- For static components: implement “Never expire” policy by setting far future Expires > header
- For dynamic components: use an appropriate Cache-Control header to help the browser > with conditional requests
雅虎建议将 Expires
字段用于所有组件,不单止于图片:
Expires headers are most often used with images, but they should be used on all components including scripts, stylesheets, and Flash components.
静态文件,请求头可以设置永不过期或者把时间设置的长一些
1 | expires: never |
对于动态组件,利用请求头的Cache-Control
控制,例如
1 | cache-control: max-age=2592000 |
Cache-Control
的值有以下几种情况:
no-cache 直接要服务的新内容,不拿缓存的
no-store 不缓存请求或响应的任何内容
max-age 响应的最大Age值
min-fresh 期望在指定时间内的响应扔有效
only-if-chache 从缓存获取资源
max-stale 接收已过期响应
min-fresh 期望在指定时间内的响应仍有效
no-transform 代理不可更改媒体类型
cache-extension 新指令标记(token)
其中,最常用的是 no-cache
,no-store
,max-age
3个值
Gzip Components *
网页中重复的内容,会用Gzip压缩,显著减少文件大小,在 Nginx 里有Nigix配置Gzip的介绍
请求头中添加:
1 | Accept-Encoding: gzip, deflate |
相应头中添加:
1 | Content-Encoding: gzip |
Configure ETags
Entity tags (ETags) are a mechanism that web servers and browsers use to determine whether the component in the browser’s cache matches the one on the origin server.
ETags
是一种机制,用来确定浏览器的缓存内容和服务器的是否匹配,如匹配,则用浏览器的内容,如不匹配,则请求服务器新的内容
请求时的头部字段:
1 | Last-Modified: Tue, 12 Dec 2006 03:03:59 GMT |
响应的头部字段:
1 | If-Modified-Since: Tue, 12 Dec 2006 03:03:59 GMT |
Browser Storage * 浏览器存储优化
Reduce Cookie Size *
Eliminate unnecessary cookies
Keep cookie sizes as low as possible to minimize the impact on the user response > time
Be mindful of setting cookies at the appropriate domain level so other > sub-domains are not affected
Set an Expires date appropriately. An earlier Expires date or none removes the cookie sooner, improving the user response time
- Cookie是请求头的一个字段,如果存储的信息过多过大,必然会影响性能,减少Cookie体积大小,只存储用户id等简单信息
- 设置合适的
expire
字段让cookie过期 - 设置cookie时,应注意设置头部字段
Set-Cookie:httponly
,只允许http通信,这样才不会被js篡改 - cookie由于是种在域名下的,请求头的一个字段,所以单独带在域名中会造成CDN的流量产生不必要的损耗,解决方法是 CDN域名和主站域名独立开来
LocalStorage *
- HTML5专门设计出来用于浏览器存储的
- 大小为5Mb左右,比cookie的4kb大很多
- 不进行通信
- 接口封装相较于cookie较好
- 浏览器本地缓存方案
SessionStorage *
- 会话级别的浏览器存储
- 大小5M左右
- 不进行通信
- 接口封装相
- 对于表单信息的维护,关闭浏览器的标签页,SessionStorage会自动清空
优化点: - 例如用户注册页面,需要填写很多东西,如果用户还没提交但刷新了该页面,用户体验会不好
- 而此时可将用户所填的信息存储到SessionStorage里,用户刷新页面时,所填写的东西也不会清空。
IndexDB
- 是一种低级API,用于客户端存储大量结构化数据。说白了,就是浏览器的数据库
- 使用的网站较少
Service Worker *
- Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。
- 使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 有拦截和处理网络请求的能力,所以必须使用 HTTPS 协议来保障安全。
- 简单点说,就是缓存js文件到浏览器里,让客户端有大量处理js的能力。
- 例如,现在的Three.js 或 WebGL可以用于3D的渲染,但3D图的js脚本及数据占用资源比较多,一个脚本可达几Mb,可以用Service Worker将其缓存起来执行。
- 利用拦截和处理网络请求的能力,可以实现离线应用功能。
检查工具:在chrome浏览器输入chrome://serviceworker-internals/
检查所有 service worker
chrome://inspect/#service-workers
检查正在运行的 service worker
Progressive Web Apps 渐进式 Web 应用
- PWA 标准由 谷歌提出的移动端的标准。例如,在移动端的弱网环境下,你站点的加载速度。离线环境下,能不能有基本的页面访问
- 可靠:在没有网络环境中也能提供基本的页面访问
- 快速:因为Web App是一个增量加载的过程,不同于iOS或者安卓的原生开发,Web App加载必受到网络条件的制约
- 融入(Engaging):将其能力对标原生APP,应用可以被添加到手机桌面,并和普通应用一样有全屏,推送等特性
CSS
CSS是存在CSS阻塞的,
- css 如果在 head 中以 link 方式引入会阻塞页面的渲染
- css 阻塞js的执行
- 但 css 不阻塞外部脚本的加载
Put Stylesheets at Top *
While researching performance at Yahoo!, we discovered that moving stylesheets to the document HEAD makes pages appear to be loading faster. This is because putting stylesheets in the HEAD allows the page to render progressively.
将样式表放在头部,可以让页面逐步呈现
Choose <link>
Over @import
In IE @import behaves the same as using at the bottom of the page, so it’s best not to use it.
在IE浏览器中 @import
和 <link>
是一样的,位于底部执行,这和我们推荐的CSS放在HEAD中执行背道而驰,所以少用 @import
JavaScript
JS 阻塞
- 直接引入js阻塞页面的渲染,所以才有后面
- js不阻塞资源加载
- js按顺序执行,阻塞后续js逻辑执行
Put Scripts at Bottom *
The problem caused by scripts is that they block parallel downloads.
While a script is downloading, however, the browser won’t start any other downloads, even on different hostnames.
JS的加载本身就是一种阻塞,所以尽量让HTML+CSS先把页面渲染出来,再执行 底部的<script></script>
标签
1 |
|
Make JavaScript and CSS External
if the JavaScript and CSS are in external files cached by the browser, the size of the HTML document is reduced without increasing the number of HTTP requests.
(除了主页) 使用CSS或JS的外部链接,浏览器会缓存 JavaScript 和 CSS 文件,而不会增加 HTTP 请求数。
1 | <link rel="stylesheet" href="/static/css/xxx.min.css"> |
Minify JavaScript and CSS *
Minification is the practice of removing unnecessary characters from code to reduce its size thereby improving load times.
最小化 JS 和 CSS,现在我们多用 webpack等打包工具来做到这一步
Minimize DOM Access *
Accessing DOM elements with JavaScript is slow so in order to have a more responsive page, you should:
- Cache references to accessed elements
- Update nodes “offline” and then add them to the tree
- Avoid fixing layout with JavaScript
最小化DOM访问,尽可能少的进行DOM操作,这点可以从现在的React
Vue
等MVVM框架体现出来
渲染优化
preload/prefetch
dns-prefetch
preload/prefetch 可控制 HTTP 优先级,从而达到关键请求更快响应的目的。
如当页面出现 Link,可 prefetch 当前 Link 的路由资源。
1 | <link rel="prefetch" href="style.css" as="style"> |
1 | <link rel="dns-prefetch" href="//shanyue.tech"> |
防抖与节流
- 防抖:防止抖动,单位时间内事件触发会被重置,避免事件被误伤触发多次。代码实现重在清零 clearTimeout。防抖可以比作等电梯,只要有一个人进来,就需要再等一会儿。业务场景有避免登录按钮多次点击的重复提交。
- 节流:控制流量,单位时间内事件只能触发一次,与服务器端的限流 (Rate Limit) 类似。代码实现重在开锁关锁 timer=timeout; timer=null。节流可以比作过红绿灯,每等一个红灯时间就可以过一批。
无论是防抖还是节流都可以大幅度减少渲染次数,在 React 中还可以使用 use-debounce 之类的 hooks 避免重新渲染。
1 | import React, { useState } from 'react'; |
虚拟列表优化
这又是一个老生常谈的话题,一般在视口内维护一个虚拟列表(仅渲染十几条条数据左右),监听视口位置变化,从而对视口内的虚拟列表进行控制。
在 React中可以用下列库