- 本文是阅读小册前端性能优化原理与实践后加上部分自己以前的理解整理而成的,更详细的原理请看小册。
从输入 URL 到页面加载完成,发生了什么?
- DNS 解析
- TCP 连接
- HTTP 请求抛出
- 服务端处理请求,HTTP 响应返回
- 浏览器拿到响应数据,解析响应内容,把解析的结果展示给用户
DNS 解析
能不能尽量减少解析次数或者把解析前置? 浏览器 DNS 缓存和 DNS prefetch,可在 html 中使用,浏览器会在页面加载完毕后进行该过程
1 |
|
另外还可以使用X-DNS-Prefetch-Control: on/off
响应头来控制预解析的开关或直接让服务器返回该响应头。
1 |
|
TCP 连接
TCP 每次的三次握手都急死人,有没有解决方案? 长连接、预连接、接入 SPDY 协议
HTTP
HTTP 优化有 2 个大的方向:
- 减少请求次数
- 减少单次请求所花费的时间
减少请求次数
-
资源整合
- 用 webpack 等打包工具,对 js 和 css 打包、压缩、删除冗余代码,避免 js 或者 css 文件过多。
- SPA 中常用的是,webpack 提取出的公用文件先加载,其他文件按照路由相关按需加载。
- 雪碧图
- Base64,适用于非常小的 logo
-
缓存
-
HTTP Cache
-
强缓存
强缓存的特点是不会与服务端发生通信,利用 http 头中的Expires
和Cache-Control
来控制,若命中,HTTP 状态码返回 200。Cache-Control
优先级高于Expires
,Expires
是一个时间戳,用的是本地时间,因为本地时间的不准确性,所以 HTTP1.1 用Cache-Control
来代替Expires
。Cache-Control
中我们用max-age
来控制资源有效期,max-age
是一个时间长度。Cache-Control
的no-cache
表示不询问浏览器,直接请求服务器(进行协商缓存),no-store
表示不适用任何缓存策略,s-maxage
只在代理服务器中生效。 -
协商缓存
协商缓存是请求服务器后,服务器来判断是返回新的资源,还是告诉浏览器使用旧的资源。命中会重定向到浏览器缓存,状态码是 304。 如果我们启用了协商缓存,它会在首次请求时随 Response Headers 返回Last-Modified
时间戳,随后的每次请求,会带上一个If-Modified-Since
的时间戳,服务器收到这个时间戳会对比该时间戳和资源在服务器的最后修改时间是否一致,如果变化,返回完整的响应内容,否则重定向缓存。Last-Modified
弊端:- 编辑文件,但是文件内容没有改变,会重新请求。
If-Modified-Since
只能检查到秒为最小单位的时间差,修改文件速度过快反而没有请求。
为了解决这样的问题,
Etag
作为Last-Modified
的补充出现了。Etag
优先级高于Last-Modified
。Etag
是由服务器为每个资源生成的唯一的标识字符串,Etag
弊端:Etag 的生成过程需要服务器额外付出开销,会影响服务端的性能
-
-
Memory Cache
memory cache
命中最快,但是它周期较短,base64 的图片,较小的 js 和 css 能够有较大几率被写进内存,这没有确定的定论。 其他较大的 js、css 和图片等会被直接写进硬盘,进行缓存。 -
Service Worker Cache
Server Worker
对协议是有要求的,必须以https
协议为前提。 我们借助Service worker
实现的离线缓存就称为Service Worker Cache
。 -
Push Cache
Push Cache 是指 HTTP2 在 server push 阶段存在的缓存Push Cache
是缓存的最后一道防线。浏览器只有在Memory Cache
、HTTP Cache
和Service Worker Cache
均未命中的情况下才会去询问Push Cache
。Push Cache
是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。- 不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个
Push Cache
。
-
-
存储
-
cookie
cookie
最大为 4k,通常用来存储一些用户登录状态,每次请求,浏览器都会带上相同域名下的 cookie。 -
webStorage
webStorage
分为Local Storage
和Session Storage
。 区别:- 生命周期:Local Storage 是持久化的本地存储,存储在其中的数据是永远不会过期的,使其消失的唯一办法是手动删除;而 Session Storage 是临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。
-
作用域:Local Storage、Session Storage 和 Cookie 都遵循同源策略。但 Session Storage 特别的一点在于,即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 Session Storage 内容便无法共享。
特性:
- 存储容量大: Web Storage 根据浏览器的不同,存储容量可以达到 5-10M 之间。
- 仅位于浏览器端,不与服务端发生通信。
-
indexDB
IndexDB 是一个运行在浏览器上的非关系型数据库。 在 IndexDB 中,我们可以创建多个数据库,一个数据库中创建多张表,一张表中存储多条数据——这足以 hold 住复杂的结构性数据。IndexDB 可以看做是 LocalStorage 的一个升级,当数据的复杂度和规模上升到了 LocalStorage 无法解决的程度,我们毫无疑问可以请出 IndexDB 来帮忙。
-
减少单次请求所花费的时间
-
资源压缩
- Gzip
进行 HTTP 压缩, 在request headers
中加上accept-encoding:gzip
就行了,可以有效减少传输文件的大小。 - 代码压缩
打包工具代码压缩 - 图片压缩
图片是网页上占用很多流量的一种资源。如果在图片损失一些颜色和像素的情况下并不会对用户体验有太大影响,那么就应该对图片进行压缩。同时可以对图片做一些裁切和缩小等操作,来减小图片的体积。
- Gzip
-
图片选用正确的格式
- PNG
无损格式,压缩率一般,支持透明背景,常用于透明图片或者 Icon 等。 - JPG
有损格式,压缩率较好,常用于复杂的大图,不支持透明背景。 - SVG
矢量图形,可编程。在各分辨率下不失真,但是渲染复杂图形较消耗性能。常用于简单图形。 - WEBP
无损格式,相较于 PNG 和 JPG 来说,压缩率更好,同时支持透明背景。唯一的缺点是兼容性不好。可用于兼容性好的浏览器,用 JPG 和 PNG 做回退机制。
- PNG
服务器 HTTP 响应
-
减少响应时间
- CDN
CDN (Content Delivery Network,即内容分发网络)指的是一组分布在各个地区的服务器。这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。 CDN 提供快速服务,较少受高流量影响。 同一个域名下的请求会不分青红皂白地携带 Cookie,而静态资源往往并不需要 Cookie 携带什么认证信息。把静态资源和主页面置于不同的域名下,完美地避免了不必要的 Cookie 的出现!
- CDN
-
降低页面初始渲染时间
- 服务端渲染(SSR)
服务器渲染最大的作用是优化 SEO。 同时在性能上也能加快首屏渲染速度,但是这样会对服务器带来一定的压力,所以需要进行综合考量。
- 服务端渲染(SSR)
页面渲染
- 页面渲染过程解析:
- 解析 HTML
- 计算样式
- 计算图层布局
- 绘制图层
- 整合图层,得到页面
-
减少阻塞
- CSS 阻塞
浏览器在构建 CSSOM 的过程中,不会渲染任何已处理的内容
,即使 DOM 已经解析完毕,只要 CSSOM 没有 OK,那么渲染就不会 OK。 所以 CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。 - JS 阻塞
JS 引擎是独立于渲染引擎存在的
,当 HTML 解析器遇到一个 script 标签时,它会暂停渲染过程,将控制权交给 JS 引擎,这是为了防止 js 操作了 dom 等情况的发生。但我们作为操作者,可以人为的指定,那些元素可以延迟加载。 为 script 标签指定 async 或 defer 来延迟脚本。 async 表示 js 不会阻塞,但会在下载完成后立刻执行。 defer 则会在下载完成并且整个文档解析完成、DOMContentLoaded 事件被触发前开始执行。
- CSS 阻塞
-
减少渲染次数
- 避免回流与重绘
回流又称为重排,即通过某种手段改变了元素的位置大小等信息,导致浏览器需要重新计算和渲染的过程。而重绘只是被改变了样式如背景和颜色等。 不论是哪一种,都会耗费性能,所以我们要避免进行循环操作。 重绘不一定导致回流,回流一定会导致重绘。 - 避免直接操作 DOM
- 避免回流与重绘
-
减少渲染节点数量
-
懒加载(Lazy-Load)
对于一些不在用户视图内的元素,我们可以在展示的时候先不进行渲染,直到该元素出现在了视图内再进行渲染。 懒加载包括对图片或者 dom 元素的加载和渲染 -
DocumentFragment 碰到大数据量的情况可以用
DocumentFragment
,DocumentFragments
是 DOM 节点,但并不是 DOM 树的一部分,可以认为是存在内存中的,所以将子元素插入到文档片段时不会引起页面回流。
-
-
提高渲染效率
- 减少 dom 节点的操作
-
事件循环(Event Loop)和异步更新
Event Loop 过程解析:- 初始状态:调用栈空。micro 队列空,macro 队列里有且只有一个 script 脚本(整体代码)。
- 全局上下文(script 标签)被推入调用栈,同步代码执行。在执行的过程中,通过对一些接口的调用,可以产生新的 macro-task 与 micro-task,它们会分别被推入各自的任务队列里。同步代码执行完了,script 脚本会被移出 macro 队列,这个过程本质上是队列的 macro-task 的执行和出队的过程。
- 上一步我们出队的是一个 macro-task,这一步我们处理的是 micro-task。但需要注意的是:当 macro-task 出队时,任务是一个一个执行的;而 micro-task 出队时,任务是一队一队执行的。因此,我们处理 micro 队列这一步,会逐个执行队列中的任务并把它出队,直到队列被清空。
因此当我们需要在异步任务中实现 DOM 修改时,把它包装成 micro 任务是相对明智的选择。
异步更新:当我们使用 Vue 或 React 提供的接口去更新数据时,这个更新并不会立即生效,而是会被推入到一个队列里。待到适当的时机,队列中的更新任务会被批量触发。这就是异步更新。
-
用节流代替防抖
节流:在某段时间内,不管你触发了多少次回调,我都只认第一次,并在计时结束时给予响应。 防抖:在某段时间内,不管你触发了多少次回调,我都只认最后一次。