
本书主要介绍用于指导前端性能优化工作的通用优化方法,从网络、浏览器、构建工具、跨端技术和CDN 等方面介绍不同技术、系统对性能的影响,同时帮助读者了解如何有效优化性能。本书从性能的度量、分析和实验这三个方面开始介绍。首先介绍性能优化的一些通用方法,然后将性能作为一个切面帮助读者了解与前端技术栈和性能有关的知识。从这个切面观察,这些系统的工作原理等知识被赋予了另外一层意义,通过这种联系把工作原理真正运用到工作中,在性能优化方面发挥重要作用。本书面向的读者为具有一定经验的 Web 开发工程师,以及对前端开发或 Web 开发有一定了解的开发人员。同时,假定读者能够进行简单的网页开发,并且具备相关的基础知识。
推荐序 对于工程师来说,性能永远是绕不开的话题。目前硬件和网络都在飞速发展,然而新的软件和交互形式在发挥想象力的同时也在最大化地发挥硬件和网络的潜力,从长远来看性能仍然是工程师关心的话题。 对于用户来说,性能同样是用户体验的核心与基础,大部分用户可能并不理解性能这个概念,但他们永远想要启动更快、响应更迅速的软件。 相比其他工程师,前端工程师是直接和用户打交道的一群人,直接对用户机器上运行的软件体验负责。当讨论后端性能时,在大部分情况下讨论的是吞吐量、并发数和响应时间等,关心的是软件运行在服务器上的性能表现。当讨论前端性能时,在更多情况下讨论的是用户感受到的白屏时间、延迟和卡顿等。这种差异使前端性能和用户体验的关联更加密切,也赋予了前端工程师独特的使命。 想要解决性能问题,开发人员需要分析现状、提出设想、进行验证,而这些都需要开发人员对相关系统的知识有足够的了解。本书从两个方面介绍了性能领域,前面介绍了“度量、分析、实验”的方法论,后面则以性能为切面介绍了前端生态与性能有关的方方面面。 读者在阅读本书时可以按照自己的需要决定阅读顺序,但至少先完整地阅读方法论部分。相比具体的知识点,方法论能为真正的性能优化工作提供通用化的思路,性能优化作为一个系统化的工程需要行之有效的思路,而不仅仅是照搬点状的知识点。 而以性能为切面也可以为读者看待前端生态提供一个不一样的视角,很多基础知识看起来和日常工作似乎没有太多的联系,但从性能的视角可以看到这些协议、工具、方案背后的思考和对实际应用的切实影响。性能并非孤立的技术领域,而是和系统中的每个环节都息息相关,为读者更深入地理解系统提供了视角。 写前端性能优化对作者有很高的要求,要有深度实践的经验并形成一定的方法论。当轩是资深开源开发人员,曾在第十五届D2前端技术论坛担任讲师。希望本书能够给广大读者带来醍醐灌顶式的帮助。 ——Node.js布道者,《狼书》作者,桑世龙(花名狼叔) 前言 性能对于开发人员来说是一个经久不衰的话题,也是用户体验的重要因素。当打开一个页面或 App 时,无论你是在寻找商品、阅读高质量的新闻,还是在看有趣的短视频,都不愿意等待。 很多人可能有耐心花费一两个小时在一家火锅店门口排队,但几乎没有人愿意等 30s 去加载一个短视频。事实上,对于大多数的 App 或网站来说,别说是 30s,即使是 3s 也足以让大量用户放弃等待转而去做其他的事情。 Google 发现,如果页面加载时间超过 3s,53%的移动网站访问活动将难以为继。 有人可能会问,如今计算机和手机的性能都在飞速发展,性能优化还重要吗?5G 时代已经来临,无处不在的高速网络是否已经让我们不需要再那么在意性能问题? 其实,在 Web 领域,安迪-比尔定律仍然成立。 安迪-比尔定律源于“Andy gives, Bill takes away.”。Andy 指的是 Intel 原 CEO 安迪·格鲁夫,Bill 则是 Microsoft 原 CEO 比尔·盖茨。这句话的意思是,Intel 一旦向市场推广了一种新型芯片,Microsoft 就会及时升级自己的软件产品,以匹配新型芯片的高性能。硬件提高的性能,很快就被软件消耗掉了。 对于 Web 领域来说,网络和终端设备的性能确实在飞速发展。然而,几十年来 Web 技术也变得越来越复杂,在网络上传输的不再是一个简单的页面。Web 技术本身也在不停地更新换代,传输页面的体积、执行脚本的复杂度等都在不断增加。 让我们回到万维网(World Wide Web)诞生的 20 世纪 90 年代,第一个网页浏览器WorldWideWeb 仅支持 HTML 格式的图片、文字和超链接。 经过几十年的发展,在网络上传输的内容越来越丰富,使用浏览器打开的不再是一个仅包含图片、文字和超链接的页面,而是高清流媒体、实时网络直播,甚至是直接在浏览器中运行的专业协同应用。 可以预见的是,这种复杂度会日益增加,越来越多原来只能在桌面平台上运行的大型软件也出现在了 Web 平台上,以借助 Web 平台易于传播、跨平台等特性,充分发挥协作互通的优势。例如,如图 0-2 所示的设计协同工具 Figma,就可以完全运行在浏览器中,而以往这种专业的设计工具只能作为桌面软件使用。 从 2011 年到 2020 年,桌面端和移动端的页面传输字节数(加载完成一个页面需要传输的数据量)逐年上涨,分别约增加了 329%和 1178%。 同时,随着网络基础建设的不断更新换代,更多原来受限于基础设施无法广泛满足的需求大量涌现。例如,近几年短视频的兴起,很大程度上就是因为大多数用户的网络能够在可以接受的时间内加载完视频。用户随时看新鲜且有趣视频的需求一直存在,只是之前的硬件条件难以满足用户的需求。 这就是安迪-比尔定律在 Web 领域没有失效的原因。可以想象,随着未来网络状况的进一步改善,又会有新的媒体和应用形式消耗提升的网络传输能力。它既可能是基于 AR、VR 的视频会议、协作办公,也可能是更加复杂、支持更多人参与协作的办公协同工具。尽管提供硬件和软件的可能不再是 Intel 和 Microsoft,但只要人们对于新功能和体验的追求一直存在,性能优化就是经久不衰的话题。 1.性能优化的魅力 上面从现实的角度出发解释了性能优化的重要性,可见重视性能优化是有客观基础的。 其实,性能优化本身就具备无可比拟的魅力。 很多人都听过斯坦门茨画一条线 1 万美元的故事,有些人说这个故事反映了知识就是财富,有些人说这个故事反映了细节决定成败。故事的真实性已经无法考证,但是笔者非常喜欢这个故事。 故事是下面这样的。美国福特公司曾经有一台电机出现故障,导致整个车间都不能运转。福特公司请来很多专家检查,就是找不到问题在哪里。于是请来斯坦门茨,斯坦门茨在电机旁聚精会神地听了 3 天,又要了梯子,爬上爬下忙了很久。最后他在电机的一个部位用粉笔画了一条线,写下了“这里的线圈多绕了 16 圈”。福特公司按图索骥解决了故障。 在平均月薪为 5 美元的当时,斯坦门茨索要了 1 万美元的酬劳:画一条线,1 美元;知道在哪儿画线,9999 美元。 如果说工程师最大的快乐来自创造,那么笔者认为第二大的快乐来自对精密系统的理解。从中不仅可以领略前人解决问题的设计方案所蕴含的智慧,还能享受抽丝剥茧最终精准找到问题的成就感。 性能优化就是一个典型场景,我们要做的是理解复杂系统并从中找到性能问题的关键所在。有时我们甚至能根据问题的表现和对系统的理解,在没有直接发现具体问题时就推测出出现问题的真正原因。例如,海王星先通过数学推算被发现再被人们实际观测到的过程就充满了预言的魔力。 海王星的发现史如下。 ? 1821 年天王星(不是海王星)的轨道表发布,但是观测表明轨道存在偏差,于是有人猜测其受到附近一个巨大天体的扰动。 ? 1846 年,通过数学方法推导出了海王星的轨道,之后开始搜寻,并于当年 9 月 23 日发现海王星。 2.性能优化 = 分析方法 + 技术原理 自工作以来,笔者有幸接触过不同场景的性能优化,包括面向计算机的和面向手机的、纯 Web 技术的和 Weex/React Native 等技术的,以及国内的和海外的。 每次接触到一个新场景,笔者都发现上一个场景的经验很难直接发挥作用,了解性能优化的读者大多听说过“雅虎三十五条优化军规”,里面总结了性能优化需要遵循的一些规则,如合并请求等。在大多数情况下,直接套用这些规则并不会为页面带来非常明显的性能收益。 但是,其背后的分析思路总是相似的,能够用来找到一套行之有效的方法帮助我们一步步接近性能目标。相比于记住正确但未必有效的具体规则,掌握这些通用的方法能让我们在复杂的生产环境中找到正确的道路。 因此,本书把“度量”、“分析”和“实验”部分放在开头部分,把方法放在具体的技术细节之前,用案例和思考相结合的方式建立面对性能问题时的解决思路,有了方法的指导,我们在遇到具体问题的时候才能进行具体分析。 如果说分析方法是解决性能问题的指南针,那么对技术原理和系统的理解就是照明灯,只有方向但看不到脚下的路是无法前行的。优化一个系统的性能也一样,即使分析出这个系统在某个阶段的性能存在问题,如果不理解系统背后的运行原理,就好像知道方向却看不见路,只能摸黑前进。 笔者在优化过程中常常根据一些有趣的知识来梳理思路,所以在介绍如何做性能优化的同时,也以性能为引子来介绍网络、浏览器、前端技术栈等的技术原理。其中的很多原理读者可能从其他地方了解过,但是带着对性能的疑问阅读本书,可以帮助读者了解更多前人为了更好的体验和性能在这些复杂系统中付出的心血。 3.关于本书 比较幸运的是,自工作以来笔者在不同的场景一直面临各种性能挑战。刚开始只是打地鼠式地逐个解决问题,在凭着猜测和主观感受进行简单的优化后就认为已经解决了性能问题。随着面临的挑战越来越多,笔者不得不思考如何科学地衡量当前的性能状况、如何科学地分析性能存在的问题和优化方向,以及如何科学地验证优化的效果。 在优化过程中,性能作为一个引子贯穿整个与 Web 开发相关的各种系统及这些系统的原理,笔者认为这些平时看起来并没有直接作用的理论知识其实可以切实帮助我们改善用户体验,并且改善的效果直接反映为数字指标。 笔者撰写本书一方面是对过去几年的工作进行总结,另一方面也希望能给想要提高用户端性能、改善用户体验但是不知道应该从哪里开始的读者一些启发。对于想在 Web 技术上更进一步的读者来说,性能也是一个非常有趣的话题和线索。 性能是一个涉及甚广且技术细节较多的话题,笔者在写作过程中尽可能搜集相关标准、资料来验证书中的细节。由于笔者的经验和视野有限,加上部分技术细节存在时效性问题,因此书中难免存在局限和疏漏,如有可改进之处欢迎各位读者批评斧正。
第 1 篇 从 Vite 起步 第 1 章 从实践开始 ··························.2 1.1 Hello World ···························.2 现在开始 ··························.2 使用 DevTools ····················.4 第一个优化 ·······················.6 1.2 现实开发的例子 ······················.7 设置开发环境 ·····················.7 Vite ································.8 vite build ··························.9 进一步优化 ·····················.11 引入 antd ························.11 按需引入 ························.13 动态 import ·····················.14 1.3 小结 ································.15 第 2 篇 性能优化方法论 第 2 章 度量 ································.18 2.1 科学的方法 ·························.19 从一个客户反馈说起 ··········.19 不度量性能,就无法优化 性能 ······························.19 真实的用户端性能 ·············.20 2.2 初识 Performance API ··············.21 performance.now()方法 ·········.21 构建首屏指标 ···················.23 2.3 均值、分位数和秒开率 ············.23 均值 ·····························.24 分位数 ···························.25 秒开率 ···························.26 如何选择合适的统计指标 ·····.26 2.4 度量首屏 ···························.27 FP ································.27 FCP ······························.27 FMP ·····························.28 如何度量 FMP ··················.28 选定并度量首屏 ················.30 2.5 度量流畅度 ·························.30 度量流畅度的指标 ·············.31 可视化工具 ·····················.31 用户端度量 ·····················.32 2.6 Core Web Vitals ····················.34 LCP ······························.34 FID ·······························.38 CLS ······························.39 2.6 小结 ·································.41 第 3 章 分析 ································.42 3.1 分析方法 ···························.43 确定目标 ························.43 收集数据 ························.43 清洗数据 ························.44 统计值分析 ·····················.44 时序分析 ························.45 维度分析 ························.46 相关性分析 ·····················.48 3.2 常用的过程指标 ····················.48 TTFB ····························.49 DOMReady 和 Load ············.50 3.3 Performance API 详解 ··············.51 Navigation Timing API ·········.51 Peformance Entry API ··········.53 Resource Timing ················.54 Navigation Timing Level 2 ·····.55 Paint Timing ····················.56 User Timing ·····················.56 3.4 分阶段性能分析 ···················.58 常用的指标 ·····················.58 其他值得分析的指标 ··········.59 3.5 小结 ································.59 第 4 章 实验 ································.60 4.1 优化不是照搬军规 ·················.61 时代在发展 ·····················.61 优化的木桶效应明显 ··········.62 用户环境差异大 ················.62 性能实验 ························.62 4.2 用实验验证优化 ···················.63 混沌问题 ························.64 设计实验 ························.64 分桶 ·····························.65 上报和分析数据 ················.68 A/B Test 背后的数学 ···········.68 结论不重要,重要的是方法 ··.69 4.3 用实验改进优化 ···················.69 建立模型 ························.69 实验修正 ························.70 4.4 小结 ································.71 第 5 章 工具 ································.72 5.1 DevTools ···························.73 Network 面板 ···················.73 Performance 面板 ···············.76 5.2 WebPageTest ·······················.81 发起测试 ························.82 报告 ······························.83 Waterfall 视图 ··················.83 5.3 小结 ································.87 第 3 篇 网络协议与性能 第 6 章 TTFB 为什么这么长 ··············.90 6.1 TTFB 的合理值 ····················.91 精确定义 ························.92 RTT ······························.92 RTT 一般需要多久 ·············.93 TTFB 的构成····················.93 实验环境验证 ···················.94 6.2 如何优化 TTFB ····················.95 减少请求的传输量 ·············.96 减少服务器端的处理时间 ·····.96 减少 RTT ························.98 TTFB 的值越小越好吗 ·········.98 6.3 小结 ·································.99 第 7 章 建立连接为什么这么慢 ·········.100 7.1 建立连接应该耗时多久 ···········.101 TCP 协议 ·······················.101 建立连接需要多少个 RTT ····.101 抓包验证 ·······················.102 7.2 如何优化建立连接的耗时 ········.103 减少物理距离 ··················.103 preconnect ······················.103 复用连接 ·······················.103 域名收拢 ·······················.104 TCP Fast Open ·················.104 QUIC 和 HTTP/3 ··············.104 7.3 小结 ·······························.105 第 8 章 Fetch 之前浏览器在干什么 ····.106 8.1 重定向 ·····························.107 HTML 重定向 ·················.109 有哪些重定向 ·················.109 8.2 浏览器打开耗时 ··················.112 初始化标签页的时间 ·········.112 unload 的耗时 ··················.112 8.3 如何优化 beforeFetch 耗时 ·······.114 重定向逻辑前置 ···············.115 合并重定向 ····················.115 避免使用短链 ·················.116 使用 beforeFetch 度量和 分析 ····························.116 8.4 小结 ·······························.117 第 9 章 HTTPS 协议比 HTTP 协议 更慢吗 ····························.118 9.1 HTTPS 协议为什么安全 ··········.119 对称加密和非对称加密 ·······.119 SSL/TLS 的实现 ···············.120 SSL/TLS 握手 ·················.122 TLS False Start ·················.124 TLS 1.3 ·························.124 9.2 HTTPS 协议如何吊销证书 ········.125 CRL ·····························.125 OCSP ···························.126 OCSP Stapling ·················.126 浏览器支持的情况 ············.126 证书类型 ·······················.127 证书验证机制对性能的影响 ···.129 9.3 HTTPS 协议更慢吗 ···············.129 确保证书链完整 ···············.129 启用 TLS 1.3 ···················.129 不滥用 EV 证书 ···············.130 开启 OSCP Stapling ···········.130 9.4 小结 ································.130 第 10 章 HTTP/2、HTTP/3 和性能 ·····.131 10.1 HTTP/2 和性能 ···················.131 连接复用为什么不生效 ······.131 头部压缩对我们有什么影响 ··.137 为什么没有广泛使用 Server Push ···························.140 10.2 为什么还需要 HTTP/3 ···········.144 HTTP/2 存在什么问题 ·······.145 HTTP/3 如何解决问题 ·······.146 10.3 小结 ······························.148 第 11 章 压缩和缓存 ·····················.150 11.1 传输速度和压缩速度如何兼得 ···.151 Content-Encoding ·············.151 gzip 压缩和 br 压缩 ··········.152 实时压缩 ······················.152 离线压缩 ······················.153 如何优化传输性能 ···········.154 11.2 HTTP 缓存什么时候会失效 ·····.154 缓存不仅仅是浏览器的 事情 ···························.154 缓存 Header ···················.154 11.3 小结 ······························.157 第 4 篇 浏览器与性能 第 12 章 浏览器和性能 ···················.160 12.1 第一次渲染时都发生了什么 ····.161 最小的渲染路径 ··············.162 尽快返回 HTML ··············.167 减少资源的阻塞 ··············.167 12.2 为什么 DOM 操作很慢 ··········.168 帧 ··································.168 重排 ·······························.169 重绘 ·······························.170 访问 DOM 属性 ················.170 如何优化 DOM 操作 ··········.171 12.3 小结 ······························.172 第 13 章 异步任务和性能 ················.173 13.1 事件循环机制 ····················.174 为什么要有事件循环 ········.174 多线程阻塞模型 ··············.174 事件循环 ······················.175 13.2 宏任务和微任务 ·················.176 13.3 Promise 的 polyfill 性能 ··········.178 如何正确实现 Promise ·······.178 13.4 requestAnimationFrame ··········.180 13.5 小结 ······························.181 第 14 章 内存为什么会影响性能 ········.182 14.1 内存 ······························.182 内存管理 ·························.183 14.2 内存泄漏 ·························.188 内存泄漏和性能 ··············.188 常见的导致内存泄漏的原因 ··.188 内存泄漏问题的诊断工具 ···.189 14.3 小结 ······························.191 第 15 章 使用 ServiceWorker 改善 性能 ······························.193 15.1 ServiceWorker 概述 ··············.194 AppCache······················.194 ServiceWorker·················.195 ServiceWorker 能做什么 ·····.195 15.2 使用 ServiceWorker 进行缓存 ···.196 Cache API ·····················.196 IndexDB ·······················.201 控制缓存的 Cache Key ······.201 更加灵活的缓存更新策略 ···.203 15.3 API 提前加载 ····················.204 15.4 ServiceWorker 冷启动 ···········.205 开启 Navigation Preload ·····.206 消费 Navigation Preload ·····.206 15.5 小结 ······························.207 第 16 章 字体对性能的影响 ·············.208 16.1 字体导致的布局偏移 ············.208 如何定位布局偏移 ···········.208 16.2 如何避免字体带来的布局偏移 ···.210 如何尽快加载字体 ···········.211 字体文件的格式 ··············.211 字体的加载 ···················.212 预加载字体 ···················.213 裁剪字体的大小 ··············.214 16.3 小结 ······························.214 第 5 篇 前端工程与性能 第 17 章 构建工具和性能 ················.218 17.1 为什么需要打包 ·················.219 CommonJS ····················.220 AMD ···························.220 CMD ···························.221 异步模块加载器 ··············.222 依赖加载优化 ················.223 模块打包器 ···················.224 ES Module ····················.225 17.2 构建工具可以做什么 ············.226 构建工具和构建优化 ········.227 为什么要优化打包体积 ·····.227 Bundle 分析 ···················.228 Tree Shaking ··················.229 Scope Hoisting ················.231 Code Splitting ·················.233 代码压缩 ······················.234 Vite 和 Bundleless ············.237 17.3 小结 ······························.237 第 18 章 服务器端渲染和性能 ··········.239 18.1 SSR 和同构 ······················.241 18.2 SSR 的性能优化 ·················.241 缓存 ·······························.242 数据预取 ·························.245 按需渲染 ·························.245 流式渲染 ·························.246 18.3 小结 ······························.246 第 6 篇 跨端技术与 CDN 第 19 章 WebView 和性能 ···············.248 19.1 WebView 和 Native 的区别 ······.249 LayoutInflater ·················.249 加载 XML 的具体过程 ······.250 Measure ························.250 Layout ·························.251 Paint ···························.252 Surface ·························.253 SurfaceFlinger ·················.253 差异 ····························.253 19.2 WebView 的通信成本 ···········.254 JavaScript 调用 Native ········.254 Native 调用 JavaScript ········.258 双向通信 ······················.258 通信对性能的影响 ···········.259 减少通信数据量 ··············.259 避免频繁通信 ·················.259 19.3 React Native 的懒加载有何 不同 ······························.260 Web 实现 ·····················.260 基于滚动容器的懒加载 ·····.260 基于位置获取的懒加载 ·····.262 虚拟列表 ······················.263 19.4 React Native 如何减小打包 体积 ······························.265 Metro ··························.265 度量 ···························.266 分析 ···························.266 手动 Tree Shaking ············.267 利用 Babel 插件进行优化 ···.269 体积和性能的关系 ···········.271 19.5 API 并行请求 ····················.271 发起请求 ·························.272 请求拦截 ·························.273 一致性检验 ······················.274 命中率分析 ······················.274 19.6 小结 ······························.274 第 20 章 CDN 和性能 ····················.275 20.1 什么是 CDN ······················.275 解析 ·······························.276 边缘节点 ·························.276 回源 ·······························.277 缓存策略 ·························.277 20.2 如何提升缓存命中率 ············.278 如何在端侧统计缓存命中的 情况 ····························.278 减少缓存分裂 ·················.279 缓存忽略动态参数 ···········.279 归一化 Vary Header ··········.280 长效缓存 ······················.280 20.3 动态加速 ·························.281 海外加速 ·························.282 连接复用 ·························.282 客户端连接复用 ················.282 HTTPS 优化 ·····················.283 动静分离 ·························.283 压缩 ·······························.284 什么场景适合使用动态加速 ··.284 20.4 自动 polyfill ······················.284 什么是 polyfill ················.284 Polyfill.io ······················.285 实现原理 ······················.287 20.5 边缘计算和性能 ·················.288 CDN 的可编程功能 ··········.288 Hello World ···················.289 自定义 Cache Key ············.289 前置重定向 ···················.290 流式渲染 ······················.290 20.6 小结 ······························.291