type
status
date
slug
summary
tags
category
icon
password
📝 前端性能优化与调试技巧
💡效能是工程师在维护项目时非常重视的要点,无论是 Web 还是 App,甚至是需要大量运算资源的机器学习,都会想追求极致的效能,用高效率换取高价值。
不过首先在文章的最开头想给读者灌输一个 mindset:“就 Web Client Side 而言(其他领域我不够了解因此先不讨论),并不是所有的应用都需要追求效能,有时候获取效能的背后也许需要花上昂贵的成本,比较起来是得不偿失的。”
因此在进行效能调校前,应该先好好衡量进行优化的成本(诸如时间、困难度等),有时先求有再求好反而是较佳的方案。另一件事即效能优化没有所谓的终点,这是一段追求摸抖嗨压苦的过程。
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ca3c22e502b34ac1b87b373bfa8fade5~tplv-k3u1fbpfcp-watermark.image?)
✒️ Web Client Side Performance Optimization
提及 Web 应用的效能优化,多数人第一时间会映入脑海中的可能会是如 Load Balancing 或是 Caching 等分流或减少网路请求的方式。
然而,这些针对 Server Side 的效能优化并非本文重点,本篇文章将概述各种 Web Client Side 效能优化的技巧。所以会比较像学生时期段考的重点复习一样,主要阐述各技巧的原理与解决了什么问题,不会详细说明实作方式。
希望能够让读者对于前端效能优化有个较宽广的理解与方向。不过会在每个技巧的段落提供一些参考链接让有兴趣更深入研究的读者自行阅读。
✒️ 为何需要在前端做效能优化?
关于这个问题我认为最主要有三点:
- SEO
- 使用者体验
- 影响营收的重要指标
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8900c37309d84be29c27d48f9322daf4~tplv-k3u1fbpfcp-watermark.image?)
据说现在网页的效能也会影响网站的 SEO Ranking 了,不过使用者体验我想才是这个问题最重要的核心。
前端主要即面向使用者,如何提供一个流畅且迅速的使用者体验是好的 Web 工程师需要去考量的,而效能则是让使用者最有感的指标。试想如果网站再精致,提供的服务再丰富,但如果需要非常长的载入时间,一般的使用者都会受不了而直接跳出页面吧。而这也带出第三个要点
影响营收的重要指标
。根据美国云端计算公司 Akamai 的研究:
当网站没有在三秒内显示完毕,40% 的消费者会选择直接跳离网站;在网站速度与营收的关系研究上,数据也表示,只要网站速度每提升100 毫秒,营收就能增加1%。
这样读者们就了解网站效能的重要性了吧(毕竟统计数据会说话呀😂
✒️ Performance Analyzers
进行效能优化前,我们得先找出效能瓶颈出自哪里,这时候我们可以使用一些 performance analyzer 来帮助我们找到问题,其中较有名的是
Lighthouse
与 PageSpeed
。PageSpeed 的话,打开浏览器 Devtool 即可看到:
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5da2f8c04cd74e1bbcdcd72171ef99f3~tplv-k3u1fbpfcp-watermark.image?)
Lighthouse 则是我较推荐的工具,界面与提供的资讯都相对完整,要使用 Lighthouse 则需先安装 Lighthouse chrome extension。
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3b1cd2479ff1437b9596af9b83a8f713~tplv-k3u1fbpfcp-watermark.image?)
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8eb37f9be59540068a14e8bb267a3bb4~tplv-k3u1fbpfcp-watermark.image?)
Lighthouse 不仅给出各项指标的分数,也会统计出渲染页面时各项工作所花费的时间,甚至会列出建议的修改方式。除了协助我们定义出问题,也协助我们对优化方式有个参考的方向。
此外,Lighthouse 更提供 lighthouse-ci 的整合服务,让专案 CI 可以与 Lighthouse 结合。每一次的 commit 皆可给出一份报表,当效能出现重大变动时就可以轻易追踪出是哪个 commit 造成的问题。
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8e0a514365584a9d83828e528ef04bec~tplv-k3u1fbpfcp-watermark.image?)
✒️ Core Web Vital
Google 根据长期以来大量的使用者体验制定出了 Core Web Vital 的指标,Google 更指出若 75% 以上的使用者在网站中的浏览体验皆能够通过以上 3 种指标,即可大幅的提升使用者的搜寻体验,甚至能让原本因等待而离开的使用者减少 24%!
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ab07e6a433b341f88f67a837e06d6c0d~tplv-k3u1fbpfcp-watermark.image?)
🔎 LCP ( Largest Contentful Paint ) — 显示最大内容元素所需时间 (速度)
LCP 是计算网页可视区 ( viewport ) 中最大元件的载入时间,也就是页面的主要内容被使用者看到的时间,是
速度
的指标。不过,可视区内最大的元素并非固定不变的:
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/04992748b5a242be81de69b98669a975~tplv-k3u1fbpfcp-watermark.image?)
上图页面在载入开始,可视区的最大元素是左上角的文字,接下来随着页面载入变成了标题,最后变成了图片。因为图片是可视区最大的元素了,因而 LCP 即以该图片所需要载入的时间做计算。
🖥️ 如何优化 LCP
- 减少伺服器回应时间
- 针对主机效能优化
- 使用较近的 CDN
- Cache
- 提早载入第三方资源
- 尽量避免 Blocking Time
- 降低 JavaScript blocking time
- 降低 CSS blocking time
- 加快资源载入的时间
- 图片大小优化
- 预先载入重要资源
- 将文字档案进行压缩
- 根据使用者的网络状态提供不同的内容
- 使用 service worker
- 避免使用客户端渲染(CSR)
- 若必须使用 CSR ,建议优化 JavaScript ,避免渲染时使用太多资源
- 尽量在伺服器端完成页面渲染,让用户端取得已渲染好的内容
🔎 FID — First Input Delay 首次输入延迟/封锁时间总计 (互动性)
输入延迟 ( Input Delay ) 通常发生于浏览器的主执行序过度繁忙,而导致页面内容无法正确地与使用者进行互动。举例来说,可能浏览器正在载入一支相当肥大的 JavaScript 档案,导致其他元素不能被载入而延迟可互动的时间。
🖥️ 如何优化 FID
- 减少 JavaScript 运作的时间
- 降低网站的 request 数并降低档案大小
- 减少主执行序的工作
- 降低第三方程式码的影响
🔎 CLS — Cumulative Layout Shift 累计版面配置转移 (稳定性)
有没有遇过一种情况是,当你在网页中准备点击一个按钮或链接的瞬间,突然一个广告被插入,让你不小心点开别的网站,恨的牙痒痒的呢?这即 CLS 指标想避免的使用者体验,如果不清楚以上情境的可以参考 这里
🖥️ 如何优化 CLS
- 给予会比较慢载入的元素一个预设的宽度与高度
✒️ Code Minimize & Uglify
有时候你会想看看一些网页的原始码是怎么运作的,不过当点选检查网页原始码后,显示出来的 code 有时却让你不知道这到底是哪个星球的代码,例如检查脸书原始码会看到:
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0ec6cf63a3cc401a98454e613eec00d9~tplv-k3u1fbpfcp-watermark.image?)
不过其实这些看起来混乱的代码其实就是我们写出来的程序,虽然变数名称跟逻辑似乎都跟我们原本开发时写的不一样,但它其实只是经过转译罢了。而这么做主要的原因有两个:
- 变数跟 code 写的越短,或是删除不必要的空白,可以省掉不少浏览器 Parse 的时间,也就是提升前端程序的效能 — Minimize
- 通常会打乱程式的逻辑,避免自家产品的 code 轻松的被别人拿去研究或抄袭 — Uglify
如果要试试看效果,可以参考如 JavaScript Minifier 或 Uglify JS 等网页服务,但通常在开发时我们不会笨拙的手动贴 code 去 Minimize 或 Uglify,而是会利用如 webpack 、gulp 等打包工具替我们做这些事情。
✒️ Image Minimize & 理解 jpg、png、 svg 的使用时机
现今的网站免不了会需要载入大量的图片,图片也因此成为网站载入资源的很大一部分,换句话说就是对网站效能有着直接的影响。在考虑 Image Lazy Load 等技巧以前,我们可以先将图片压缩,通过减少档案大小来加快载入时间,而压缩又分为两种状况:
- 有损压缩:如 JPG,使用只取部分像素资料的方式来压缩图片大小,并且压缩后是不可逆的。
- 无损压缩:如 PNG,压缩后不影响图片品质。
而其实不同的图片类型也有各自适合使用的时机,学会将不同类型图片应用在适合的地方不仅可以提升使用者体验或 UI 品质,某些状况下也可以控制载入资源的大小而提升一点效能。这里推荐 一篇文章,说明得十分完整,除了有介绍各种图片类型的适当使用时机外,也有介绍如响应式图片、webp、Image CDN 等其他图片优化技巧,非常推荐一读。
✒️ Critical Render Path 关键渲染路径
提及网页前端的效能最佳化,我们得先了解网页是如何渲染到页面上的,从收到 HTML、CSS 和 JavaScript,再对代码进行必需的处理,到最后转变为显示像素的过程中还有许多中间步骤。将效能最佳化其实就是了解这些步骤中所有的活动,再经过最佳化,这就是所谓的关键渲染路径 Critical Render Path 。
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/567e52baaaea437ba9e0dbf2560241ad~tplv-k3u1fbpfcp-watermark.image?)
根据上图,我们可以大致理解出网页渲染的流程为:
- 读取 HTML 后生成 DOM Tree
- 读取 HTML 中的 CSS Link Tag 生成 CSSOM Tree
- DOM Tree 与 CSSOM Tree 共同生成 Render Tree
- 根据 Render Tree 生成 Layout
- 最后 Paint 画面
当然,现今的 Web App 不太可能只靠 HTML 跟 CSS 就完成,还是得靠 JavaScript 来修改网页的内容、样式、与使用者互动的行为。 JavaScript 可以查询及修改 DOM 和 CSSOM,在 CSSOM 执行完毕后,JavaScript 才会执行 。
这边给一个小 tip:如果可以的话,CSS file 尽快引入,JS 在 CSS 后引入,因为 JS的执行会导致网页载入的暂停(不过有例外的非同步功能,很快就会讲到了)。
一般我们要载入 JavaScript 档案,通常会透过
<script>
这个 Tag 来达成,不过它的执行是同步的,也就是会导致网页载入的暂停,如下图:![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/00f4963a8a19428da9b8472fd3625979~tplv-k3u1fbpfcp-watermark.image?)
但其实 script tag 的引入还有 async 跟 defer 这两种方式:
async 会非同步去请求外部脚本,回应后停止解析执行脚本内容。
defer 也会非同步请求外部脚本,但是等待浏览器解析完才执行。
<script async>
用于载入第三方函式库等不需要动到 DOM 结构的状况:![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2be50d8c06a34a0d9259285b28779c58~tplv-k3u1fbpfcp-watermark.image?)
<script defer>
要整个页面都下载及分析完成后才会执行,非常类似于把 JS 放在页尾的情况:![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/248ce77da1634ca38a0e2eebb329ebac~tplv-k3u1fbpfcp-watermark.image?)
因此在适当的时机选用不同的载入方式,是有机会提升网页的效能的,对于 Critical Render Path 或资源载入方式有兴趣的读者,可以更进一步阅读 google developer 的文章 或 MDN script tag 的 document。
✒️ Code Splitting
Code Splitting 是一个非常重要的观念,现代网页程式渐渐走向使用框架以模组化方式来开发,即便会透过如 webpack 等 bundler 来 uglify、minimize、打包程式码,当专案成长到一定程度时,程式 bundle size 仍然会变得过于肥大,导致 client side 的网页载入时间变长,严重影响使用者体验。Code Splitting 就是为了要解决单一 JS Bundle 过于肥大的问题,将原本单一的 bundle 切分成数个小 chunk,可以搭配平行载入,或者是有需要时才载入某些特定的 chunk,又或是对一些不常变动的 chunk 个别做快取,来达到载入效能的优化。
较常见的 Code Splitting 又分为两种方式 :
- 抽离第三方套件
- 动态载入功能模组 Dynamic Import
🔎 抽离第三方套件
抽离第三方套件又可细分两种方式:
- 将所有第三方套件打包为单一档案
- 将第三方套件打包为多个档案
🔎 将所有第三方套件打包为单一档案
这边主要参考前年 Modern Web 你的 JS 该减肥了! 5 个提升网页载入速度的技巧 这个议程的内容。关于 webpack 的 bundle,可以先做一个最大的拆分:
- Application Bundle:UI 与商业逻辑,跟我们写的程序有关,是经常变动的部分。
- Vendor Bundle:第三⽅套件 /node\_modules,不太会变动。
拆分出 Vendor Bundle 是有好处的,主要是因为通常它变动的频率相对较低,因此比较适合被 cache,而在 Vendor Bundle 被 cache 的状况下由于减少了 Application Bundle 的⼤⼩,因此加快了再访者的载入速度。
采用这样的方式的优点为逻辑简单,缺点为更新任何第三方套件都会使快取失效。
🔎 将第三方套件打包为多个档案
采用这种方式的优点是可以根据套件关联性打包,减少套件更新时造成的延迟。缺点则是相较前面打包成单一档案的方式,这种方式需要处理的逻辑复杂许多。
透过 webpack 的 CommonsChunkPlugin 实作类似下图的 config 可以达成这个效果:
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d09ba4124a2747508f2d0010ca2444e8~tplv-k3u1fbpfcp-watermark.image?)
🖥️ Webpack Bundle Analyzer
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/50dc800ceebb4a13ac3e4abafa7786f7~tplv-k3u1fbpfcp-watermark.image?)
通过 webpack-bundle-analyzer,我们可以透过视觉化分析专案有哪些 bundle chunk,各个 bundle chunk 的组成又为何,再针对可以改进的 bundle 进行优化。
(类似功能的工具还有如 WebpackVisualizerPlugin)
🖥️ 动态载入功能模组 Dynamic Import
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4249a0472f4c48a5b61a4fc4da1e11d4~tplv-k3u1fbpfcp-watermark.image?)
大多数状态下我们会在档案的开头引入需要用到的模组,这些模组通常在网页载入时就被引入进来,这种方式被称为 static import,然而当有以下两种状况的需求时,static import 却不能满足我们:
- 模组名称为动态变数时
- 需依照特定逻辑或特定时机引入时
这时候可以运用与之相对的技术:Dynamic Import。所谓 Dynamic Import 代表的即是:
需要用到某段代码时才透过网络载入 JS bundle
要实现 Dynamic Import 需要靠 ESM import 语法:
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5276942cbaac463e826ab9ed3143f1b7~tplv-k3u1fbpfcp-watermark.image?)
例如上图我们在 getComponent 这个函式中 import lodash 这个 package,只有当 getComponent 被呼叫时 lodash 才会被当成另外一个 chunk 载入。
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d8988ce59ec14102a1ef81e41fc79f98~tplv-k3u1fbpfcp-watermark.image?)
目前浏览器的支援度也还算不错,我们也可以透过 webpack 等打包工具来帮助我们实现 Dynamic Import。
参考链接:
了解了 Dynamic Import 的概念,接下来来谈谈 Dynamic Import 的使用情境,今天主要会介绍两种情境:
- 根据路径做 Dynamic Import
- 针对肥大套件做 Dynamic Import
🖥️ 根据路径做 Dynamic Import
根据 GA 等分析工具长期分析后的数据指出,大部分的使用者只会停留在网站中的几个热门页面。如果采用 Client-Side-Rendering 的方式建置网站的话,在没有对 bundle 做额外处理的状况下会在一开始载入 JS bundle 时就载入许多页面的资源,这样会导致许多不太会被使用者浏览的页面是很有机会被载入却又没被使用的。
这时候我们可以选择针对路径做 Dynamic Import,当切换到特定路径时再载入该路径会用到的资源。因为笔者擅长 React,所以就以 React 中的著名路由套件 react-router 搭配 React.lazy 来举例:
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a30ff9db88d448beb12b225c040aa0b0~tplv-k3u1fbpfcp-watermark.image?)
造访 / 时,Home component 将会被载入,而造访 /about 页面时则是 About Component 会被载入,这也就是基于 Route 的 Code-Splitting。
(当然 Component 的 code splitting 也不一定只能做 route based 的,开发者可以自己视情况对 component 做 Dynamic Import,例如 React.Lazy 就是为此而存在,不过 React.Lazy 目前还无法在 SSR 使用,如果使用 SSR 建构专案的读者可以参考 Loadable Components)
參考链接:React Official Doc
🖥️ 針對肥大套件做 Dynamic Import
除了针对路径做 code splitting 以外,另一种常见的方式就是针对 肥大却又不会马上用到 的模组做 Dynamic Import,这时前面介绍过的 Webpack Bundle Analyzer 就展现价值了,有了视觉化的报表,开发者可以依据图表判断是不是有过于肥大的套件适合做 Dynamic Import。
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/500e04d38acc46b3b85e8765bd7eaa53~tplv-k3u1fbpfcp-watermark.image?)
✒️ Lazy Load Image
稍早的 Image Minimize 段落中有提到图片占了网站资源相当大的比例,因此如果在网页载入的瞬间就想把所有图片都载入下来对效能是一个硬伤。
这时候可以采用 lazy load 的方法去载入图片,一开始只需载入部分的图片,待现有图片快要接触到 viewport 的底部时再去动态载入新的图片,例如 imgur 这种图床网站就势必会做图片的 lazy load。
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/94682b5f323a4da1a416b1e7078de9f3~tplv-k3u1fbpfcp-watermark.image?)
要实现 image 的 lazy load 主要有两种方式:
- 搭配 Intersection Observer web API : 侦测目标元素是不是与特定位置交会,交会时再去载入新的资源。
- 浏览器原生支援 ( 參考链接 )
至于浏览器原生支援的部分,未来可能只需要在 img tag 加上 loading=lazy
就可以自动帮我们做好 lazy load,不过目前浏览器支援度还不太普及,就再拭目以待啰!
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d60c3143e9004003b80a69dc55f67930~tplv-k3u1fbpfcp-watermark.image?)
另外能够 lazy load 的不只是图片而已,例如经过 pagination 的 API data 也很适合做 lazy load 喔!
✒️ Virtualized List
长列表(例如大量的文章列表)是网站中蛮常见的一个 feature,然而如果有 1000 篇文章,我们又将这些文章同时渲染的话,就必须生成 1000 个 dom 节点,更不用说文章结构通常是相对复杂的,像这样同时渲染数量颇大的元素会有几个明显的缺点:
- 载入时白屏时间会比较长
- 渲染了大量的 dom 节点的状况下,在滚动事件触发时会大大增加记忆体的用量
- 容易失帧,因为渲染很慢,所以无法维持浏览器的帧率,页面会显得卡顿
- 最惨的话网页会失去响应
而且这些问题在 Desktop 浏览器就会发生了,换作是手机浏览器只会让问题变得更严重,因此这种状况下我们应该优化长列表,提升使用者体验。
virtualized list 就是优化长列表的一种技巧,名字听起来很深奥,不过它的概念其实非常简单:
用阵列储存所有列表元素的位置,只渲染可视区 ( viewport ) 内的列表元素,当可视区滚动时,根据滚动的 offset 大小以及所有列表元素的位置,计算在可视区应该渲染哪些元素。
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/baee69aa934d4878a27550955d5baadc~tplv-k3u1fbpfcp-watermark.image?)
以上图来说,假设可视区最多只能显示 6 个 item,那即使我们的列表总共有 1000 个,也只会渲染出现在可视区的 6 个元素,当原本被渲染的 item 移出可视区后,就会被 unmount 掉,避免前面说的同时生成一堆 dom 节点的状况,也因此有效的解决了上面说的几个缺点。
如果对如何实作一个 virtualized list 有兴趣,可以参考 這篇文章。
也有许多现成的 virtualized list 套件例如 react-window 或是 vue-virtual-scroll-list。
✒️ Tree Shaking
开发项目免不了会下载第三方套件来节省自己重复造轮子的成本。然而,也许某些状况我们只会使用一个套件模组之中的特定几个 function,其他的 function 几乎都不会用到。不过如果我们为了这几个 function 而要载入整个模组,就似乎有点得不偿失了。
这时候 Tree shaking 会是解救我们的技巧。
🔎 什么是 Tree Shaking ?
其实这个技巧跟字面上的意思很像,当用力摇一棵树时可能会把很笨重的果实给摇落,在程式面来说就是把用不到的代码给摇落下来。
上面的例子讲到我们可能会为了几个特定函式而需要载入整个套件,运用 Tree Shaking 之后,可以让打包工具在打包阶段就可以分析哪些 code 或哪些 function 是用不到的,而把它们从最终的 bundle 中剔除。换句话说就是确保最后的 bundle 不会包含无用或多余的程式码与资源,减少 bundle size。
🔎 如何做到 Tree Shaking ?
要做到 Tree Shaking,首先得透过 ES6 import export 的帮忙
假设我们要使用 array-utils 中的某几个函式,应该避免上面的引入方式(把整个 array-utils 引入进来,再去使用特定的 property),而改为下面这种引入方式:
不过此时如果用打包工具例如 webpack 打包的话,还是会将整个 array-utils 加进 bundle 里,此时还得靠例如 uglifyjs-webpack-plugin 或其他的 plugin 再加上一些额外设定才能把用不到的程式从 bundle 中移除。
(关于CommonJS 与ES6 的区别,还有为什么可以实现Tree Shaking,请参考 这篇文章,关于 webpack 针对 Tree Shaking 的设定可以参考 webpack 官方文件)
✒️ Preload、Prefetch And Others In Link Tag
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9bbf7298bbb3426abd58c5fcd54ca99b~tplv-k3u1fbpfcp-watermark.image?)
这边要介绍五个特殊的 link 技巧:prefetch、preload、preconnect、dns-preconnect、prerender。读者可能看起来一头雾水,不过其实它们都有一个共同的目标,或者说共同的效能优化方式:
对不久的将来会用到的资源预先处理,这里的处理有可能是载入资源,或是建立连线,因此在真的要使用到该资源时可以省去不少时间。
🔎 Preload VS Prefetch
preload 与 prefetch 是两个较常被搞混的技巧,两者的作用都是在提早取得将来会用到的资源,然而两者的差别在于:
- Preload:取得当前页面的资源(例如字体 font)。
- Prefetch:告诉浏览器 “这些资源我待会会用到,先帮我下载吧!” 不过与 preload 不同的是 prefetch 抓取的资源不限于当前页面使用,也就是可以跨越 navigation,例如你很确定使用者会点击下一页,就可以使用 prefetch 预先抓取下一页的资源。
浏览器对于资源的载入顺序是有规则的,是以档案类型来决定下载的优先顺序,以 chrome 举例来说(不确定不同浏览器是否不同):
- High priority : style /font / XHR (sync)
- Medium priority : 位于可视区域的图片 / Preload without as/ XHR (async)
- Low priority : favicon、script async / defer / block、不在可视区域的图片、媒体档、SVG 等
preload 与 prefetch 也是以属性来分辨档案类型
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d4624e04dad9484fae3048dd6cfaa955~tplv-k3u1fbpfcp-watermark.image?)
chrome 资源载入顺序对照表
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d2e0cc53a0564f1482b81147cfb796e9~tplv-k3u1fbpfcp-watermark.image?)
浏览器 devtool 的 network tab 也可以看到各资源的 priority
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bd23d6b31612434a908f0b8275cc5602~tplv-k3u1fbpfcp-watermark.image?)
🖥️ Preconnect
preconnect 相当于告诉浏览器:"这个网页将会在不久的将来下载某个 domain 的资源,请先帮我建立好连线。"
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/de00db6174b94a3aaa54189f9b83a013~tplv-k3u1fbpfcp-watermark.image?)
要理解 preconnect 能够达成的事,得了解浏览器在实际传输资源前,实际上经过哪些步骤(以下内容与图片参考 这篇文章 ):
- 向 DNS 请求解析网域名
- TCP Handshake
- ( HTTPS connection ) SSL Negotiation
- 建立连线完成,等待拿到资料的第一个byte
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1dfc1b9dfd1844ef8bf64a857382f0fc~tplv-k3u1fbpfcp-watermark.image?)
上面的四个步骤中,每一步都会需要一个 RTT ( Round Trip Time ) 的来回时间。所以在实际传输资料之前,已经花了 3 个 RTT 的时间。如果在网路状况很差的状况下,会让获取资源的速度大大降低。
利用 preconnect 提早建立好与特定 domain 之间的连线,省去了一次完整的 ( DNS Lookup + TCP Handshake + SSL Negotiation ) ,共三个 Round Trip Time 的时间。
🖥️ Preconnect Use Cases :
通常只会对确定短时间内就会用到的 domain 做 preconnect,因为如果 10 秒内没有使用的话浏览器会自动把连线 close 掉
- CDN:如果网站中有很多资源要从 CDN 拿取,可以 preconnect CDN 的域名,这在不能预先知道有哪些资源要抓取的情况,是蛮适合的 use case。
- Streaming 串流媒体 (待会看下方 lite-youtube-embed 的例子)
🖥️ DNS Preconnect
跟 preconnect 类似,差别在于只提示浏览器预先处理第一步 DNS lookup 而已。也就是说
dns-preconnect = DNS look up\ preconnect = DNS look up + TCP Handshake + SSL Negotiation
至于什么时机要使用哪个方式,可以参考 这篇 stackoverflow 问题
🖥️ Prerender
prerender 比 prefetch 更进一步。不仅仅会下载对应的资源,还会对资源进行解析。解析过程中,如果需要其他的资源,可能会直接下载这些资源,基本上就是尽可能预先渲染下个页面,这样一来当用户在从当前页面跳转到目标页面时,浏览器可以快速的响应。适合用在用户很高机率会转到另一个页面的状况下使用
不过浏览器支援度有点低,笔者目前也没试过这种方式,就留给有兴趣的读者自行研究了。
🖥️ Preconnect + Preload Example:lite-youtube-embed
lite-youtube-embed 是一个号称渲染速度比原生 iframe 快 224 倍的 youtube 影片播放元件,它能达成这样的效能提升其实做的事并不复杂,主要有两件事:
- preload youtube 影片的 thumbnail (预览图),让使用者可以尽早看到预览图,提升使用者体验
- 当使用者鼠标移到元件范围时,对 youtube domain 进行 preconnect,当使用者真的点下播放键时才真正载入iframe,不过因为有对 youtube domain 做 preconnect,因此省去 3 个 Round Trip Time 的时间,因此可以更快速开始播放影片。
✒️ CDN & Cache
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dd79de5954b342118e1449aa5ba59cfb~tplv-k3u1fbpfcp-watermark.image?)
CDN 的全名为 Content Delivery Network 内容传递网路。
要知道距离不仅仅是爱情的毒药(误),也是影响 response time 的重大因素。像笔者现在人在台湾,跟一个架设在台湾的 server 取资料,花费的时间只要 500 ms,但如果去跟一个架设在美国的 server 取相同的资料,这时候的 response time 可能就增长为 3000 ms。
CDN 就是通过在各个地理位置建立 edge server 来避免取资源时都要跟距离遥远的 server 沟通,造成效能的低落。当使用者对被 CDN 加速过的域名发出 request 时,CDN 会自动将 request 导到地理位置离使用者较近或是流量较不吃紧的 edge server,尽管第一次取资源时因为 CDN 还没有快取的资料,所以仍然需要跟 original server 要资料,不过之后的 request 就可以透过地理位置离使用者较近的 CDN cache 取得,加快 client 端资源载入的速度。除了 cahce 机制以外,CDN 某方面也算是增强了服务的可用性、负载功能、安全性(降低 DDOS 对网站的影响)。
如果要更深入了解 CDN 或者 networking cache,推荐参考 这篇铁人赛的优质文章。
而目前最热门的免费 CDN 服务大概就是 Cloudflare 了, 它主要提供的服务有:
- CDN
- Cache
- Load Balancing
- 代管 DNS
- 阻挡恶意流量
有兴趣的读者赶紧去体验看看吧!
✒️ Write Good Code
其实并不是实作文章到目前为止介绍的技巧才能做到效能优化,开发者平常在写 code 的时候就该多注意自己写的 code 是否会对效能造成影响,例如:
- 会不会造成不必要的重新渲染 (Re-render)?
- 事件监听器 (Event Listener) 在用不到时是否正确被移除?
- 撰写的 Function 应该注意一下是否有时间复杂度更低的解法,或是会不会造成不必要的 memory 浪费?
- 擅用适合情境的 Design Pattern,除了提高程式码可读性与可维护性外,也有优化效能的机会。
- 如果是 React 开发者(笔者是 React 狂粉),注意是否有些地方可以运用 useMemo、React.memo、useCallback 等优化技巧(当然其他框架应该也有对应的解法)
虽然在文章的开头有说到有时「先求有再求好」反而是较佳的方案,不过我们却应该在平时写code 的时候就去避免写出效能不佳与不好维护的程式码,不然最后要做效能优化的时候,可能还得面对自己留下的技术债,解决难度又提升了不少。
✒️ 多参考其它成功的效能优化案例
透过参考其他公司的效能优化成功案例,可以得知如何化理论为实作,还有执行效能优化后具体的数据成效为何,例如载入时间减少多少、bundle size 减少多少、或是效能优化后真实的使用者回馈等,都可以作为自己专案的参考,以下附上几篇知名企业的 web 效能优化案例:
✒️ 结语
想通过一篇文章讲完 Web 效能优化这个主题还是稍嫌勉强,虽然看起来提到了很多技术,但仍然有非常多优化技巧是没有机会提到的,而每个技巧也只能讲解大略的概念(如果真要认真研究,任一个主题用一篇文章应该都是讲解不完的)。不过文章内提供了许多参考连结,读者可以再根据有兴趣的技巧深入研究,希望这篇文章可以让读者对于 Web 前端的效能优化有个大概的认识与方向。
- 作者:墨綠B.G.
- 連結:https://www.blackishgreen.link//article/front-end-performance-optimization-and-debugging-skills
- 著作權:本文採用 CC BY-NC-SA 4.0 許可協議,轉載請註明出處。