性能优化专题
June 12, 2022
开始
从用户开始来访问你的网站应用,到最终他在上面浏览信息点击操作,其中经历了非常多的环节,而每个环节都可能带来性能问题,那每个环节也都是我们实现性能提升的机会,但茫茫多的环节我们有时根本无从下手,因此我们性能优化的第一步就是要了解我们的页面都经历哪些环节,并且都有哪些问题。
在了解了整体情况之后我们需要总结出每个环节的用时,然后制定一个指标。比如大指标是首屏加载 2s 缩短至 1s,那么这 1s 的缩减就要分摊到各个环节,可能有些环节本身就是达标的,所以我们只要盯紧那些关键环节就好
那我们接下来就先了解一下各个环节与一些通用的解决方案
首屏
从输入 url 到页面打开经历的过程
我们每个 html 页面都会经历上述的几个过程,因此我们对于此阶段的性能优化其实有一些通用的手段,下面我将会更进一步描述一下这几个过程且提供几种通用的加快速度的手段
URL 解析
用户输入 url 按回车,浏览器进程此时会检查 url,组装协议,构成完整的 url,然后通过 IPC 把 url 请求发送给网络进程
缓存
网络进程接收到 url 请求后会检查本地缓存是否存在,缓存分为好几种
本地缓存:LocalStorage、sessionStorage、indexedDB。假如我们的页面上有一些功能是一段时间不会变化的,例如日榜单,我们就可以在当天第一次请求时将结果存储到 localStorage 中,之后便不发送请求而直接去 localStorage 中拿
内存缓存:这一部分是浏览器行为,我们并不能过多操作
Cache API: service worker 等,service worker 可以拦截我们应用中发出的请求,并针对拦截的请求做优化
Http 缓存:http 缓存分为强缓存和协商缓存,强缓存就是在本地缓存没过期时间内将直接使用本地缓存,不会询问服务器;而协商缓存需要询问服务器资源是否过期,如果过期则另取,没过期则用本地
DNS 解析
通过 DNS 解析 url 获取真正要请求的 IP 地址,首先会检查本地 hosts 是否有映射,其次 DNS 本身也会有缓存。
这一部分基本是浏览器行为,我们没有过多的介入的能力,但是浏览器还是提供了一种手段来加快此进程,那就是 DNS prefetch。它是浏览器提供的一个 API,用于告诉浏览器预先解析一下这个 url 的 ip 地址,当然,浏览器并不保证一定会去预先解析域名,可能会根据当前的网络、负载等状况做决定。
发起请求
利用 ip 地址与服务器建立 tcp 连接,如果是 https 则还需要再建立 tls 安全通道,建立完连接后开始发送 HTTP 请求
- resourse hint 可以用于辅助浏览器来做资源优化
preconnect:预先建立连接
prefetch:资源预先获取
prerender:资源预获取且预执行
preload:资源会立刻进行预先获取并存到内存中,与 Prefetch 相比,Preload 会强制浏览器立即获取资源,并且该请求具有较高的优先级(mandatory and high-priority),因此建议对一些当前页面会马上用到资源使用 Preload;相对的,Prefetch 的资源获取则是可选与较低优先级的,其是否获取完全取决于浏览器的决定,适用于预获取将来可能会用到的资源。
- 采用 CDN 加快请求速度
服务器响应
服务器响应,网络进程接受响应信息,浏览器会先检查状态码,如果是 301/302 要重定向,错误码的话会直接终止,如果是 200 响应,会检查 Content-Type,如果是字节流类型,则直接交给下载器,如果是 html 的才会通知渲染进程进行渲染
渲染
网络进程会把下载的资源流到渲染进程中,渲染进程当收到字节流数据后即刻开始顺序解析,解析的流程大致如下图所示
- 将 HTML 内容转换为能够读懂的 DOM 树结构,将 CSS 转化为 styleSheets,两者一般情况下为并行解析,( 但是因为 JS 会阻塞 DOM 的解析,而 JS 的解析又依赖 CSS 的解析,所以这部分的流程可能因为 JS 的加入而变得混乱,这个我们先不展开,后续将会专门探讨)。
- 根据 DOM tree 与 styleSheets 会生成 renderTree,renderTree 会隐藏掉不需要渲染的节点,并且分好层级
- 根据渲染树将节点绘制在页面上,以上几个步骤并不是一次性完成,如果 DOM 或者 CSS 被更改时会被重复执行,如果不影响元素的位置层级等信息时会只触发 repaint,而当要更改元素位置时便会触发 reflow,reflow 的操作包含 repaint,代价也会更高,因此要尽量避免不必要的 reflow
因为渲染进程是顺序解析,并且 JS 会阻塞 DOM 的构建,所以我们要注意 JS 资源的位置,尽量保证首屏的 HTML 与 CSS 尽可能的早解析完毕
对于 JS 来说,我们一般有几种常用的处理方法
- JS 代码通常情况下放在 body 的最后面
- 对于一些与主业务无关的或者不操作 DOM 的 JS 使用 async 或者 defer
- 减小 Js 代码体积,比如代码压缩、按需加载等方法
- 代码压缩
运行时
这部分主要影响我们页面的操作手感与体验,比如页面是否流畅
- 因为 Js 会操作 DOM 与 CSS 进一步触发 reflow 与 repaint,因此要注意尽量避免不必要的重渲染
- 尝试使用 requestAnimationFrame 与 requestIdleCallback ,这两个方法都会在浏览器相对空闲的时候去调用
- 做好防抖与节流
- Passive event listeners ,浏览器需要将方法执行完才知道要不要阻止默认行为,因此如果该方法调用时间很长并且不阻止默认行为的话可以添加
{passive: true}
让浏览器在运行前就知道 - 表格列表虚拟滚动,避免过多元素的展示
- 做好过渡效果
…等
结尾
以上所说的环节是基本每个页面都会经历的,给出的解决方法也都是通用的解决方法,但是其实每个页面的展示都有其独特的轨迹,要想最大程度提升我们页面的性能,我们必须要在了解上述内容的前提下去分析出我们自己页面的关键路径。
当然上述整套流程其实只是性能优化的一次大体检,我们之后最好还是需要建立一个性能优化的检测机制,去自动化的检测我们页面上的性能问题,毕竟性能优化不是一锤子买卖,我们的性能问题也会随着业务与需求的拓展而增加。
详情页性能优化点
- html 里添加 loading,可以让 html 解析完之后立马出现 loading
- api 分析
- 微应用详情页,数据传递,任务和项目的数据传递过来,首屏打开加快,之后还是自己调用接口,因为微应用是分而治之的想法
- 沙箱复用,不会重复 mount 与 unmount
- debounce