前端性能优化

我们提要的优化知识点主要来自 Chrome 和 V8 引擎,使用者众多

如何优化

  1. 第一位不是具体手段,而是先收集数据

  2. 收集数据之前需要了解页面从加载到呈现的步骤(越详细越好)

  3. 有了数据可以很清晰的了解我们的短板

  4. 如果不确定哪里是短板可以参考别家的数据

  5. 结合我们的产品形态制定目标和基准

  6. 根据目标“寻找”具体手段进行“优化”

这是可能诸位经常碰到的一个棘手的问题,当然 它也是一道面试高频题,很多人处理或者回答这个问题的时候,第一反应就是优化手段,诸如:压缩图片、合并资源、减少请求、SSR 等等。如果你有实战过前端优化的化,可能就不是这个答案了。这并不是说上述的优化手段有问题,具体手段当然很重要,但没有任何相关性能数据收集的话,一切都是浮云,我们很难知道性能短板在哪里。平时开发工作比较繁重,补丁摞补丁加班比雨后春笋还多,虽然这不是一个性能低下烂代码的理由,但这是客观存在的问题…

因此我们在开发业务的时候除了架构设计要合理、注重代码逻辑清晰简洁高效之外,很难了解页面整体的性能情况,所以比较正常的思路是:找到合理的性能统计方法并且收集具体的数据,对症下药。当然了,在收集数据之前,我们需要先了解一些浏览器的渲染原理和一些概念,否则很容易吃错药,出现头痛医脚的情况。

浏览器展示原理

旁白

这又是一个基础知识,当然也是个面试高频题。我很喜欢这个问题,知识点几乎是可以无限扩充的,基本知识点都掌握了,基本上也就了解优化的奥义了。首先先从宏观的角度看一下,用户输入 url 之后,页面是如何呈现的,先看看这张图

数据加载步骤

  1. 如果当前页面有正在显示的文档话,则卸载当前页面

  2. 同时检测有没有缓存,这里的缓存指的是离线缓存(manifest)或者 PWA 设置的缓存并不是咱们平时提到的浏览器强制或者协商缓存,有则读取缓存,否则继续 DNS 解析(这里多说一句:关于这部分缓存可以看一下的WHATWG 的离线缓存规范,WHATWG == Web Hypertext Application Technology Working Group 即网页超文本应用技术工作小组,W3C 宣布与 WHATWG 达成协议,HTML 和 DOM 标准都以 WHATWG 为准,也就是说以后只有一套 HTML 标准了)

  3. DNS 解析

  4. 建立 TCP 链接,由于网络层的 IP 协议是无状态的,协议只负责把数据包发送到指定 IP,不会考虑前面是否已经发送过数据包,也不考虑后面还会不会发送数据包。和渣男一样哈,三不原则,不主动、不拒绝、不负责,当然应用层的 UDP 也继承这个行为。不过 TCP 就不同了,是个负责的协议,为了保证传输就要建立连接。如果你要是使用 HTTPS 的话,当然这一步里还藏着个安全连接的握手过程

  5. 浏览器发送请求

  6. 服务器接到请求处理,返回给浏览器数据,这一步从浏览器角度来看也叫做数据下载

  7. 最后就是浏览器获取数据后处理数据渲染页面了

具体渲染步骤

由于已经有前人栽树了,我就乘个凉,借花献佛了浏览器渲染原理,这个是已经离开我们多时的一位前端同学的一篇浏览器渲染文章,以前这个同学也分享过。所以粗略的说一下,浏览器渲染又分为:

  1. 把 HTML 结构字符串解析转换为 DOM 树,构建 DOM 树完成后,触发 DomContendLoaded 事件。

  2. 下载并解析 CSS 产生 CSS Rule Tree

  3. 下载 JS 并通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree

  4. 解析完后,浏览器引擎会通过 DOM Tree 和 CSS Rule Tree 来构造 Rendering Tree

    • 这里要注意一下,还是老生常谈的问题:
      • 在标签没有设置 async/defer 属性时,JS 会阻塞 DOM 的生成
      • JS 文件不只是阻塞 DOM 的构建,它会导致 CSSOM 也阻塞 DOM 的构建
      • 当然他们最终也是会阻塞整个页面的渲染,如果是在页面打开的第一次就会出现所谓”白屏“的问题
  1. 布局 Rendering Tree(Layout/reflow),负责各元素尺寸、位置的计算

  2. 绘制 Rendering Tree(Paint),绘制页面像素信息

  3. 浏览器会将各层的信息发送给 GPU(GPU 进程:最多一个,用于 3D 绘制等),GPU 会将各层合成(composite),显示在屏幕上

由于 JavaScript 代码运行在浏览器的主线程上,与此同时,浏览器的主线程还负责样式计算、布局、绘制的工作,如果 JavaScript 代码运行时间过长,就会阻塞其他渲染工作,Composite 是个例外,如果你开启 GPU 加速的话,这部分会在 GPU 进程去运行。阻塞和渲染的原理就说的这里,感兴趣的同学可以好好翻翻这篇文章,学习一下。

JS 引擎工作原理

词法分析

语法分析

生成字节码

即时编译(JIT

  • 词法分析:字节流解码器会先从代码字节流中创建 令牌 (token),每当一个 令牌 创建后,就会被传递给 解析器(parser)

  • 语法分析:解析器便会根据传过来的令牌创建出 抽象语法树 (Abstract Syntax Tree)

  • 字节码:AST 被生成之后,接下来就要交给 解释器(interpreter) 了。解释器会遍历整个 AST,并生成 字节码。当字节码生成后,AST 便会被删除以节省内存空间。最终我们得到了更贴近 机器码 的 字节码。这里的 字节码 是介于 AST 和 机器码 之间的一种代码,它还是需要通过 解释器 将其转换为 机器码 后才能执行

  • 即时编译:尽管 字节码 很快,但是它还可以更快!解释器在逐条解释执行字节码时,会分析是否有某段代码被多次执行,这样的代码被称为 热点代码。热点代码 和生成的 类型反馈 (type feedback) 会被发送到一个称为 优化编译器 的东西中,然后由它转换为可以直接被电脑执行的 机器码,这样在下次执行这段代码的时候就不需要再编译了,从而大大提升了代码的执行效率。

  • 感兴趣的话可以看看这篇文章

事件循环

由于 JS 是个单线程语言,单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。为了解决这个问题 Eventloop 事件循环机制就诞生,这里面分为同步任务和异步任务。同步任务是调用立即得到结果的任务,同步任务在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务是调用无法立即得到结果,需要额外的操作才能预期结果的任务,异步任务不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

需要注意的是,虽然异步任务分为宏任务和微任务,但上面提到异步任务是宏任务,微任务没有这个功效。宏任务包括了:

  1. 浏览器加载的 JS 脚本

  2. 事件处理函数

  3. 定时任务(setTimout,setInterval ,setImmediate)

  4. IO 处理(网络请求、磁盘读写)

  5. UI 渲染;

而微任务是 JavaScript 引擎在执行每一个宏任务之后,会立刻执行微任务队列中所有任务之后,再执行宏队列中的其它任务或者进行渲染,因次对于渲染来说,微任务并没有什么帮助,微任务重了,依然解决不了主线程阻塞的问题。那关于事件循环的具体知识点可以看看 B 站这个熟肉

收集数据

数据加载耗时:单点+用户收集

上面就是比较宏观的浏览器知识和概念,全都了解过原理之后我们再看一下具体的指标和概念就非常好理解了。回到之前看过的图

这个图描述的是W3C Web 性能工作组带来的 PerformanceTiming,一个 W3C 的规范,我们可以通过浏览器的API,去获取这些数据。根据上图描述,我们可以计算出资源加载在网络上的一些耗时

1
2
3
4
**let** dns = domainLookupEnd - domainLookupStart,//dns耗时
tcp = connectEnd - connectSta
ttfb = r0.responseStart - startTime,//获取首字节耗时
ssl = secureConnectionStart?connectEnd - secureConnectionStart : 0;//https握手耗时

*参考资料

指标 含义
navigationStart 当卸载提示在同一浏览上下文中的上一个文档终止时. 如果没有以前的文档,则此值将与 PerformanceTiming.fetchStart 相同
unloadEventStart 引发了 unload >事件后,指示窗口中上一个文档开始卸载的时间. 如果没有先前的文档,或者先前的文档或所需的重定向之一不是同一来源,则返回的值为 0
unloadEventEnd unload 事件处理程序完成时. 如果没有先前的文档,或者先前的文档或所需的重定向之一不是同一来源,则返回的值为 0
redirectStart 当第一个 HTTP 重定向开始时. 如果没有重定向,或者其中一个重定向源不同,则返回值为 0
redirectEnd 当最后一个 HTTP 重定向完成时,即已收到 HTTP 响应的最后一个字节. 如果没有重定向,或者其中一个重定向源不同,则返回值为 0
fetchStart 当浏览器准备好使用 HTTP 请求获取文档时. 此刻在检查任何应用程序缓存之前
domainLookupStart 域查找开始时. 如果使用持久连接,或者信息存储在缓存或本地资源中,则该值将与 PerformanceTiming.fetchStart 相同
domainLookupEnd 域查找完成后. 如果使用持久连接,或者信息存储在缓存或本地资源中,则该值将与 PerformanceTiming.fetchStart 相同.
connectStart 当打开连接的请求发送到网络时. 如果传输层报告错误,并且连接建立再次开始,则给出最后的连接建立开始时间. 如果使用持久连接,则该值将与 PerformanceTiming.fetchStart 相同
connectEnd 打开连接网络时. 如果传输层报告错误,并且连接建立再次开始,则给出最后的连接建立结束时间. 如果使用持久连接,则该值将与 PerformanceTiming.fetchStart 相同. 当所有安全连接握手或 SOCKS 身份验证终止时,连接被视为已打开
secureConnectionStart 安全连接握手开始时. 如果不请求此类连接,则返回 0
responseStart 当浏览器从服务器从缓存或本地资源接收到响应的第一个字节时
responseEnd 当浏览器收到响应的最后一个字节时,或者如果第一次发生则关闭连接时,包括来自服务器,缓存或本地资源
domLoading 解析器开始工作时,即其 Document.readyState 更改为’loading’并且引发了相应的 readystatechange 事件
domInteractive 解析器完成对主文档的工作时,即其 Document.readyState 更改为’interactive’并且引发了相应的 readystatechange 事件
domContentLoadedEventStart DOM 解析完成后,在解析器发送 DOMContentLoaded 事件之前,网页内资源开始加载的时间
domContentLoadedEventEnd DOM 解析完成后,在所有需要尽快执行的脚本(无论是否按顺序执行)之后
domComplete 解析器完成对主文档的工作时,即其 Document.readyState 更改为’complete’并且引发了相应的 readystatechange 事件
loadEventStart 为当前文档发送 load 事件的时间. 如果尚未发送此事件,则返回 0
loadEventEnd 当 load 事件处理程序终止时,即加载事件完成时. 如果此事件尚未发送或尚未完成,则返回 0

前端性能耗时:单点收集

参考资料

  • 总概念

    • 蓝色(Loading):网络通信和 HTML 解析
    • 黄色(Scripting):JavaScript 执行
    • 紫色(Rendering):样式计算和布局,即重排
    • 绿色(Painting):重绘
    • 灰色(other):其它事件花费的时间
    • 白色(Idle):空闲时间

-

无法复制加载中的内容

  • 详细

    • Interactions:交互
    • Main:主线程
    • Worker:web work 线程
    • Raster:光栅图像(也称为“位图”)
    • GPU:GPU 渲染
    • Chrome_ChildIOThread:用来接受来自其它进程的 IPC 消息和派发自身消息到其它进程
    • Compositor:合成

当然你英文不好的话,可以下载使用微软最新版本的edge,目前最新版本的 Win10 俗称 sp2 的 20H1 版本月底将要 rtm 了,也是内置这个版本的 edge,这个版本的 edge 最大的优点可以理解为===chrome,并且还是个全汉化版本,当然他汉化了你也不一定能看到懂,他的画风是这样的。

chrome 官方文档:https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference

旁白

目前前端和 SRE 一起开发的 EA 全链路监控系统前端 sdk 部分就是根据这个 api 去开发的,感兴趣的同学可以看一下这篇文章:https://hotoo.github.io/blog/post/resource-timing-practical-tips

大家注意到上面那张图还有条红线,红线前半部分是网络耗时,后面的是浏览器解析 HTML 文档的耗时,不过后面 Load 和 DOMContentLoaded 等事件并不能真正的体现页面加载的性能,因为它们并不是与用户在屏幕上看到的相对应,我们需要更用户的一些的数据,因此可以使用 Chrome 自带的 Performance 工具来查看

*名词解释:FP、FCP、DCL、L、FMP、LCP、TTI、FID 😑

  • FP(First Paint): 页面在导航后首次呈现出不同于导航前内容的时间点,FP 事件在图层进行绘制的时候触发,而不是文本、图片或 Canvas 出现的时候

  • FCP(First Contentful Paint):这是当用户看见一些“内容”元素被绘制在页面上的时间点。和白屏是不一样的,它可以是文本的首次出现,或者 SVG 的首次出现,或者 Canvas 的首次出现等等

  • 注意:只有首次绘制文本、图片(包含背景图)、非白色的 canvas 或 SVG 时才被算作 FCP。FP 与 FCP 这两个指标之间的主要区别是:FP 是当浏览器开始绘制内容到屏幕上的时候,只要在视觉上开始发生变化,无论是什么内容触发的视觉变化,在这一刻,这个时间点,叫做 FP。相比之下,FCP 指的是浏览器首次绘制来自 DOM 的内容。例如:文本,图片,SVG,canvas 元素等,这个时间点叫 FCP。FP 和 FCP 可能是相同的时间,也可能是先 FP 后 FCP。

  • DCL(DomContentloaded): 当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完成加载.

  • L(onLoad), 当依赖的资源, 全部加载完毕之后才会触发.

  • FMP(First Meaningful Paint):基于 Chromium 的实现,这个绘制是使用 LayoutAnalyzer 进行计算的,它会收集所有的布局变化,当布局发生最大变化时得出时间,而这个时间就是 FMP,本质上是通过一个算法来猜测某个时间点可能是 FMP,所以有时候不准。

  • LCP:了解和测量网站真实的性能其实非常困难,像 load 和 DOMContentLoaded 不会告诉我们用户什么时候可以在屏幕上看到内容。而 FP 和 FCP 又只能捕获整个渲染过程的最开始,FMP 更好一点,但是它的算法比较复杂,而且前面说了,有时候不准。根据 W3C Web 性能工作组的讨论和 Google 的研究,发现测量页面主要内容的可见时间有一种更精准且简单的方法是查看什么时候渲染最大元素,LCP 等于这个元素开始渲染的时间

  • TTI (Time to Interactive) 可交互时间: 指标用于标记应用已进行视觉渲染并能可靠响应用户输入的时间点

摘自:https://web.dev/lcp/

  • FID (First Input Delay) 首次输入延迟: 指标衡量的是从用户首次与您的网站进行交互(即当他们单击链接,点击按钮等)到浏览器实际能够访问之间的时间

寻找短板

制定目标

从业务和用户体验的角度确认关键路径非关键路径让路

首页

播放页

审批页

我入行以来,参与过很多次优化,其中大兴土木的有 3 次:

  • 视频网站首页和播放页的优化

  • 视频 App 启动优化(android)

  • 咱们现在的审批优化

不同的性质的项目,从业务和用户体验的角度来看要求肯定是不一样的,所以目标也是完全不同的,不过也有相似点,给关键路径让路。

比如上面提到的视频网站首页,关键路径是让用户尽快看到第一屏的焦点图和推荐内容,其他信息可以让路;播放页用户肯定关注的视频,所以其他内容可以让路;视频 App 套路也一样,不过 app 现在首屏广告位是一大收入,那广告就要第一时间显示,我之前东家就有一项专利,在电视启动的时候,先来一发广告,本身这只是为了在 android 启动的时候替换启动系统带来的无聊时间,类似 windows 的启动画面,结果这个方案已经被滥用到,广告时间严重超过启动时间了,万恶的商人;我们的审批页重点是让用户最先看到关键审批信息并可以操作审批按钮。

关于关键路径再多说一句,一个页面的元素很多,逻辑也不少,要找准重要流程,对症下药。制定关键路径的时候一定要对用户行为进行分析,因为关键路径的优化势必影响非关键路径的展现或操作,换句话来说也就是对于某些逻辑就是要有意识的略化,正所谓鱼翅熊掌不能兼得。比如小米手机现在的急速启动方案,看起来启动确实快了,但你会发现手机启动之后诸如 nfc 等后台程序并没有一起加载,而是延后了很久。我就是上了这个当,因为每天乘地铁上班,用手机刷公交卡,有一次手机重启,我溜溜在闸机旁边等了 1 分多种,最后才发现其实还可以手动加载。但是这种关键路径已经满足了大多数人的需求,我这个遭遇相对小众,所以这么做无可厚非。回来再说我们的优化,不同的目标也就意味着我们要梳理重要流程的逻辑,指定好大多数人的关键路径上,把好钢用在韧上。比如上面提到的视频网站播放页的优化,页面刚加载的时候无论是网络还是 cpu 都很忙,我们要忙中偷闲,把资源留给重要的流程,所以页面上如评论区、内容介绍,推荐区等等其他的都需要给视频第一帧让步。同样我们的审批页也是如此,比如合同审批页面,从用户角度来说看到审批内容,操作审批按钮是重要的事情,其他的诸如审批流程、讨论区等等相对不是重要流程,因此可以给关键路径让让路,躲一躲。

具体手段

网络传输:减少体积,善用缓存

*连接池限制:同时 6 个,多了等待

参考资料

浏览器 HTTP 1.1HTTP 1.0IE 6, 724IE 8, 966Firefox 1366Chrome 206?Safari 5.1.76?Opera 11.648?

https://source.chromium.org/chromium/chromium/src/+/master:net/socket/client_socket_pool_manager.cc

1
2
3
4
5
6
7
8
9
10
11
12
// Default to allow up to 6 connections per host. Experiment and tuning may
// try other values (greater than 0). Too large may cause many problems, such
// as home routers blocking the connections!?!? See http://crbug.com/12066.
//
// WebSocket connections are long-lived, and should be treated differently
// than normal other connections. Use a limit of 255, so the limit for wss will
// be the same as the limit for ws. Also note that Firefox uses a limit of 200.
// See http://crbug.com/486800
int g_max_sockets_per_group[] = {
6, // NORMAL_SOCKET_POOL
255 // WEBSOCKET_SOCKET_POOL
};

为了解决这个问题我们需要把一些太过细碎的资源请求合并,解决连接池阻塞的问题,所以

  1. 合并接口

  2. 合并静态资源

合并之后还有的好处,可以利用 TCP(HTTP)慢启动特性

旁白

  1. 由于 TCP 协议的限制,PC 端只有 65536 个端口可用以向外部发出连接,而操作系统对半开连接数也有限制以保护操作系统的 TCP\IP 协议栈资源不被迅速耗尽,因此浏览器不好发出太多的 TCP 连接,而是采取用完了之后再重复利用 TCP 连接或者干脆重新建立 TCP 连接的方法。

  2. 如果采用阻塞的套接字模型来建立连接,同时发出多个连接会导致浏览器不得不多开几个线程,而线程有时候算不得是轻量级资源,毕竟做一次上下文切换开销不小。

  3. 这是浏览器作为一个有良知的客户端在保护服务器。就像以太网的冲突检测机制,客户端在使用公共资源的时候必须要自行决定一个等待期。当超过 2 个客户端要使用公共资源时,强势的那个邪恶的客户端可能会导致弱势的客户端完全无法访问公共资源。从前迅雷被喷就是因为它不是一个有良知的客户端,它作为 HTTP 协议客户端没有考虑到服务器的压力,作为 BT 客户端没有考虑到自己回馈上传量的义务。

*TCP 特性:越下越快

慢启动:最初的 TCP 在连接建立成功后会向网络中发送大量的数据包,这样很容易导致网络中路由器缓存空间耗尽,从而发生拥塞。因此新建立的连接不能够一开始就大量发送数据包,而只能根据网络情况逐步增加每次发送的数据量,以避免上述现象的发生。具体来说,当新建连接时,cwnd 初始化为 1 个最大报文段(MSS)大小,发送端开始按照拥塞窗口大小发送数据,每当有一个报文段被确认,cwnd 就增加 1 个 MSS 大小。这样 cwnd 的值就随着网络往返时间(Round Trip Time,RTT)呈指数级增长,事实上,慢启动的速度一点也不慢,只是它的起点比较低一点而已。我们可以简单计算下:

开始 cwnd = 1
经过 1 个 RTT cwnd = 2*1 = 2
经过 2 个 RTT cwnd = 2*2= 4
经过 3 个 RTT cwnd = 4*2 = 8

旁白

这么看来是不是都放在一起更好呢,并不是~首先你并不能确认每个服务器下载速度都能>=你的全速带

宽,再考虑到丢包重试问题,以及浏览器的多线程下载,而且数据下载完才能解析,而浏览器运行是多线程,这部分一直在下载,其他线程在空闲也是不合理的,所以要权衡,因此回到之前的说法,给重要的路径来让路。我们可以把跟关键的资源放在一起,非必要的资源切分。一句话概括,以业务逻辑为主该合并的合并,该切分的切分。不过现在我们用到的 vue 有一个比较麻烦的地方,渲染逻辑和交互逻辑都放在一起了,只能合并打包。理想的情况是是模版和交互逻辑分离,预先模板加载。比如以前比较古老的 JQuery 开发方式就可以很好的分离。不过我们依然可以从展示的角度,把目前的关键路径组件隔离出来优先加载执行。这里再说一下,关于如何合并可以做到极致也不用太过纠结,目前我们现在数量最多的浏览器是 chrome,他是支持 http2 协议的,而 H2 协议有链路复用的功能

*HTTP2:多路复用

https://www.nginx.com/blog/7-tips-for-faster-http2-performance/

所以这部分可以不用特别极致,反而稍微多一些比大一些更好,按照关键路径理顺就成了。

减少体积:刚性优化

减少体积俗称减肥,感谢这个时代,我们有了 webpack,他有一些非常好用的减肥药,比如:webpack-bundle-analyzer。

旁白

这个插件,可以分析我们每个入口 js 文件的依赖。据此,我们可以了解到每块资源的大小和依赖关系,看看我们引用的组件是否全都用到了,没有的话就去除,为了个很小的功能引入的话可以考虑局部模块引入,如果非得全部引入的话,就得考虑我们或者第三方组件设计问题了,这时就需要手动拆卸出逻辑引入了。我之前整理项目曾经看到过为了一个时间格式化功能,引入了一个整个包含多国语言包的 moment.js,这基本上就是为了用捡到个鼠标垫买了台电脑的思路。

缓存:减少下载

  • 协商缓存

  • 强制缓存

    • 自动
    • 手动

旁白

浏览器缓存分为强制缓存和协商缓存两种,强制缓存不需要直接从本地磁盘或内存读取,而协商缓存 ETag/if-None-Match 和 Last-Modified/if-Modify-Since 虽然不需要下载,但还是要建立 TCP 连接去获取资源更新信息,因此我们尽量使用强制缓存。而强制缓存还可以分为浏览器自动强制缓存,自动指的是浏览器自身的策略,我们可以在服务器上设置资源返回的 ResponseHeader,Cache-Control: max-age=时间(秒)即可,只要设置得当基本上可以满足大多数需求,不过要是想极致一些就考虑手动方案了,具体实现方式有:

  • localStorage:把一些要路径的代码以字符串的方式存到本地,使用时获取

  • Application Cache:做一个缓存清单,浏览器根据清单的指示进行读取

  • Progressive Web App(PWA):PWA 其实不止缓存这么简单,还可以做诸如信息推送等功能,而且缓存内容代码可控

目前从我们的浏览器统计来看,PWA 可以大规模应用了,字节内部的网站很多都用到了这个功能,当然我们做审批优化的时候也做了,不过目前只是缓存了静态资源,终极方案可以把 HTML 文档全都缓存,基本上就可以理解为是个 APP 了,完全没有任何态资源加载,只有运行和接口加载的耗时了。当然这也是个双刃刀,像小程序和 APP 一样,需要做一些发版更新策略,可能有些用户体验上的问题,这也是我们迟迟未上的原因

逻辑顺序调整:Loading、异步加载、预加载

  • 从体验上讲,看到白屏确实不好,不过这时候可以让用户“愉快”的等待:loading 图片或骨架屏,不管哪种方式,都需要加载资源。这部分资源可能包含 js css 和 图片,就建议写在 html 文档里了。试想如果你把这部分资源打到 js 里,岂不是用户还得走之前的下载解析过程,要白屏了?万一比业务代码还慢呢?

  • 代码加载(业务逻辑)顺序依然可以用这个工具检查一波,把非关键路径上的业务异步化。根据业务逻辑,异步分为主动异步和被动异步。比如我们的审批页面,就可以把讨论区和流程显示部分组件和数据加载异步化。主动异步的意思是在没有用户操作的情况下进行异步加载,比如用户停留在这个页面上,不进行任何操作,浏览器发现目前计算资源空闲了,就可以加载了。还可以被动异步,用户滚动页面时候,检测到用户可见区域就去加载一下数据。https://www.toutiao.com/

  • 最后就是预加载了,简单来说,就是把本次要做的事情放在更早去做,比如Fast API 方案简述 ,或者目前做的审批预加载

*其他问题常规思路

  • Chrome 自带的工具

  • 减少主线程单次任务复杂度

  • 少回流、少重绘、能合成就合成

旁白

除了上面说的优化之外,还有些纯前端性能的优化,比如解决卡顿或者操作不流畅问题。这些问题还得需要用到最开始提到 Chrome 的火焰图工具,看看时序图有没有什么问题,恭喜你现在事件循环和浏览器渲染的知识全都用到了。首先看看有没有 long task,这上面带红色三角的就是 chrome 认为过长的任务,会在主线程阻塞渲染,先看看为什么长,看一下 js 的堆栈信息,如果有代码运算大的情况先考虑是否有不合理的地方可以优化,再者就是切分任务了,把一个过长的任务切分成多个,解决主线程耗时过长的问题。比如 React16 的 fiber 虽然实现策略是挺复杂的,但根本原理还是就是把任务切分了。除了 task 之外还可以从布局、渲染部分进行优化,原则很简单:少回流、少重绘、能合成就合成,万一避免不了回流,可以考虑经常回流的层独立,用局部回流的方式降低整体回流的消耗。

*不要迷信:比如 SSR

  • 最后要说的就是,千万不要迷信优化方法,一定要收集完整的数据,找准目标,再做打算,切勿盲目入手。比如现在比较流行的 SSR,也就是服务端渲染,这和以前 JSP 原理一样,不同的时候可以大部分复用相同的前端代码用 Nodejs 在服务端页面 html 渲染好,直接输出到前端。我们可以分析一下这种优化要解决什么问题,在什么情况下才能起到优化的效果。

  • 浏览器端程序无法”流式“加载(非绝对),浏览器中 HTML 就是流式加载的代表:边下载边解析边展示

  • 数据和模板特别复杂,运算量非常大,受众的客户端机能浏览器性能很差,用户端渲染非常慢

  • 第一个场景,如果我们是个长内容网站,内容数据量远远大于我们的静态资源,那么这种方案就很合适,服务端输出的 HTML 内容可以流式加载,不用拿到全量数据就能把第一屏数据及早的展示给用户。但对于我们现在的审批页面,要渲染的内容才几 k,静态资源远远大于内容,这种方式显然就没有什么意义,况且该加载的前端资源一点儿也没少, 渲染部分每个人的输出内容还不同,还丧失了用户端的缓存功能,非常的不划算。

  • 第二个场景,对我们来说意义也不一定很大,首先目前我们的工作电脑配置还可以而且用户以高性能的 Chrome 为主,浏览器端渲染并不是大问题,我们还可以把模版和资源做缓存,如果缓存没有刷新,下次用户在再次开的话就不用重新下载了,看起来效果会更好一些。

持续监控

  • 上面这些都是具体执行的手段,如果碰到解决就好了,但如果要想做到持续优化约束,就要指定基准,如果不达到基准不能算测试通过。基准最好前后端分开指定,这样可以有针对性的进行持续自动化测试。这里先说说前端吧,定义基准数据前一定也要把测试机的基准配置定定义好,否则针对端的基准数据就没有任何意义了,一台在奔三 800 定义的基准在 i9-10900K 没有任何意义,反之也毅然,要想有效测试最好在同等配置相同操作系统相同版本相同负载的机器上做。我们再根据实际情况定义好各个指标的数据,比如:scripting、reflow、reapint,自动化测试获取数据,看看每次是否达到预定的要求

非前端优化

  • 上面说的都是前端的优化,除此之外就是后端接口本身业务逻辑耗时以及网络本身因素的影响。不管前端如何优化,如果上面那两部分有问题的话,从整体上来看仍然很难达到预期的目标。当然这个问题还是需要信息收集。我们就要用到最开始介绍的概念了,通过 performance 接口收集这些数据进行分析,看数据加载到底慢在哪个步骤了,本来我司有 Tea 和 Slardar 工具,不过都无法达到我们细致分析的目的,因此有了全量数据收集服务的想法,凑巧和 SRE 的 OWL 联系起来,于是就有了我们 EA 的全链路监控方案,用这套方案可以定位到很具体的问题,继续对症下药。比如我们可以定位到是否为后端本身业务慢,后端可以做一些的优化:比如前置处理并缓存,把要实时计算的东西,找个再早的时机,计算出结果放在缓存里,像审批来说,可以上一节点审批完就可以把下一节点计算好放在缓存里前端直接获取;或者异步化提交,也就是用户提交操作后立刻把数据返回给前端,后端再去离线处理;以前有个弹幕的项目,不需要时实行很高,我们直接把热弹幕放在 cdn 上了,既减轻了服务端的压力,又能用到 CDN 的布点优势,当然我这些都是从逻辑上的技术想法,后端同学做这些还有处理巨多的业务问题,可能就需要个后端优化分享了。再说说网络,用目前的全链路工具也能看出用户本身网络问题,不过还不太能定位具体情况,毕竟 C 端的情况太复杂了。比如用户设备建设本身速度慢,中间经过的链路过长,WIFI 信号不好等等,这些问题就太复杂了,估计就得需要 SRE 的同学来处理了。我还记得在前司,很多同学都私搭烂建自己的 WIFI 热点,搞得物理 WIFI 信号串扰严重,特别影响用户体验。但是我们还可以根据地域方式把这些数据继续细分,找出些端倪,以前做 C 端的时候,运维同学每个月绩效全指着这些数据呢,比如看到重庆慢了,布个点,刷一下速度就上来了,后天云南慢了等等。所以数据越详细越能挖掘出问题,粒度太粗的话,很难定位的。

参考手段

WebWorker

WebAssembly

TreeSharking

接口合并(BFF)

设置服务器缓存

耗时任务切片执行

开启服务器压缩(如 gzip 等)

requestIdleCallback

requestAnimationFrame

开启 HTTP2

dns-prefetch、Preload、Prerender、Preconnect

固定类型的函数

CSS transform

代码压缩

减少 JSON 里的空 Key

media 属性指定加载样式的条件

异步加载

多次操作 DOM 时可以尝试,首先克隆整个 DOM 节点更加高效,操作克隆后的节点,然后替换

合并资源

压缩图片(渐进式、webp)

loading=“lazy”

节流和防抖

减少 base64

一些脑洞

WebRTC 资源 P2P

Websocket 长连接精简数据量和连接数量

Sass less 代码本地转换(PWA)

Http2 push

vue 渲染业务分离