2019 年 02 月 11 日阅读 32
本文永久链接:
译者: https://github.com/wznonstop
校对者: https://github.com/TUARAN , https://github.com/xilihuasi
让 2019 来得更迅速吧! 你正在阅读的是 2019 年前端性能优化年度总结, 始于 2016.
[译] 2019 前端性能优化年度总结 - 第一部分
[译] 2019 前端性能优化年度总结 - 第二部分
[译] 2019 前端性能优化年度总结 - 第三部分
[译] 2019 前端性能优化年度总结 - 第四部分
[译] 2019 前端性能优化年度总结 - 第五部分
[译] 2019 前端性能优化年度总结 - 第六部分
目录
交付优化
39. 是否所有的 JavaScript 库都采用了异步加载?
40. 使用 IntersectionObserver 加载开销大的组件
41. 渐进式加载图片
42. 是否发送了关键的 CSS?
43. 尝试重组 CSS 规则
44. 有没有将请求设为 stream?
45. 考虑使组件具有连接感知能力
46. 考虑使组件具有设备内存感知能力
47. 做好连接的热身准备以加速交付
48. 使用 service workers 进行缓存和网络后备方案
49. 是否在 CDN/Edge 上使用了 service workers, 例如, 用于 A/B 测试?
50. 优化渲染性能
51. 是否优化了渲染体验?
交付优化
39. 是否所有的 JavaScript 库都采用了异步加载?
当用户请求页面时, 浏览器获取 html 并构造 DOM, 然后获取 CSS 并构造 CSSOM, 然后通过匹配 DOM 和 CSSOM 生成渲染树. 一旦出现需要解析的 JavaScript, 浏览器将停止渲染, 直到 JavaScript 被解析完成, 从而造成渲染延迟. 作为开发人员, 我们必须明确告诉浏览器不要等待 JS 解析, 直接渲染页面. 对脚本执行此操作的方法是使用 HTML 中的 defer 和 async 属性.
在实践中, 事实证明我们应该更倾向于使用 defer. 使用 async 的话, Internet Explorer 9 及其之前的版本有兼容性问题 https://github.com/h5bp/lazyweb-requests/issues/42 , 可能会破坏它们的脚本. 根据 Steve Souders 的讲述, 一旦 async 脚本加载完成, 它们就会立即执行. 如果这种情况发生得非常快, 例如当脚本处于缓存中时, 它实际上可以阻止 HTML 解析器. 使用 defer 的话, 浏览器在解析 HTML 之前不会执行脚本. 因此, 除非在开始渲染之前需要执行 JavaScript, 否则最好使用 defer.
此外, 如上所述, 限制第三方库和脚本的可能造成的影响, 尤其是社交分享按钮和嵌入式 <iframe>(如地图).Size Limit 库 https://github.com/ai/size-limit 可以帮助防止 JavaScript 库过大 : 如果不小心添加了一个大的依赖项, 该工具将通知你并抛出错误. 可以使用静态的社交分享按钮 (例如 SSBG https://simplesharingbuttons.com ) 和交互式地图的静态链接.
也可以试着修改非阻塞脚本加载器以实现 CSP 合规性.
40. 使用 IntersectionObserver 加载大型组件
一般来说, 延迟加载所有大型组件是一个好主意, 例如大体积的 JavaScript, 视频, iframe, 小部件和潜在的图像. 最高效的方法是使用 Intersection Observer API, 它对具有祖先元素或顶级文档视口的目标元素提供了一种异步观察交叉点变化的方法. 基本用法是, 创建一个新的 IntersectionObserver 对象, 该对象接收回调函数和配置对象. 然后再添加一个观察目标就可以了.
回调函数在目标变为可见或不可见时执行, 因此当它截取视窗时, 可以在元素变为可见之前开始执行某些操作. 实际上, 我们使用了 rootMargin(根周围的边距)和 threshold(单个数字或数字数组, 表示目标可见性的百分比)对何时调用回调函数进行精确控制.
Alejandro Garcia Anglada 发表了一篇关于如何将其应用到实践中的简易教程, Rahul Nanwani 写了一篇关于延迟加载前景和背景图片的详细文章, Google Fundamentals 提供了关于 Intersection Observer 延迟加载图像和视频的详细教程. 还记得使用动静结合的物体进行艺术指导的长篇故事吗? 你也可以使用 Intersection Observer 实现高性能的滚动型讲述 https://github.com/russellgoldenberg/scrollama .
另外, 请注意 lazyload 属性, 它将允许我们以原生的方式指定哪些图像和 iframe 应该是延迟加载. 功能说明: LazyLoad 将提供一种机制, 允许我们强制在每个域的基础上选择加入或退出 LazyLoad 功能(类似于内容安全政策的功能. 惊喜: 一旦启用, 优先提示 priority hints 将允许我们在标题中指定脚本和预加载资源的权重(目前已在 Chrome Canary 中实现).
41. 渐进式加载图片
你甚至可以通过向页面添加渐进式图像加载技术将延迟加载提升到新的水平. 与 Facebook,Pinterest 和 Medium 类似, 可以先加载质量较差甚至模糊的图像, 然后在页面继续加载时, 使用 Guy Podjarny 提出的 LQIP(低质量图像占位符)技术将其替换为原图.
对于这项技术是否提升了用户体验, 大家各执一词, 但它一定缩短了第一次有效的绘图时间. 我们甚至可以使用 SQIP https://github.com/technopagan/sqip 将其创建为 SVG 占位符或带有 CSS 线性渐变的渐变图像占位符. 这些占位符可以嵌入 HTML 中, 因为它们可以使用文本压缩方法自然地压缩. Dean Hume 在他的文章中描述了 如何使用 Intersection Observer 实现此技术.
浏览器支持怎么样呢? 主流浏览器 https://caniuse.com/#feat=intersectionobserver ,Chrome,Firefox,Edge 和三星的浏览器均有支持. WebKit 状态目前已在预览中支持. 如何优雅降级? 如果浏览器不支持 intersection observer, 我们仍然可以使用 来延迟加载或立即加载图像. 甚至有一个库 https://github.com/ApoorvSaxena/lozad.js 可以用来实现它.
想成为一名发烧友? 你可以追踪你的图像 https://jmperezperez.com/svg-placeholders/ 并使用原始形状和边框来创建一个轻量级的 SVG 占位符, 首先加载它, 然后把占位符矢量图像转换为 (已加载的) 位图图像.
https://jmperezperez.com/svg-placeholders/
José M. Pérez https://jmperezperez.com/svg-placeholders/ 的 SVG 延迟加载技术.(大图预览)
42. 你是否发送了关键的 CSS?
为了确保浏览器尽快开始渲染页面, 通常做法是收集开始渲染页面的第一个可见部分所需的所有 CSS(称为 "关键 CSS" 或 "首页 CSS")并将其以内联的形式添加到页面的 "" 中, 从而减少往返请求. 由于在慢启动阶段交换的包的大小有限, 因此关键 CSS 的预算大小约为 14 KB.
如果超出此范围, 浏览器将需要额外的开销来获取更多样式. CriticalCSS https://github.com/filamentgroup/criticalCSS 和 Critical https://github.com/addyosmani/critical 使你能够做到这一点. 你可能需要为正在使用的每个模板执行此操作. 如果可能的话, 请考虑使用 Filament Group 使用的条件内联方法, 或动态地将内联代码转换为静态资源.
使用 HTTP/2, 关键的 CSS 可以存储在单独的 CSS 文件中, 并通过服务器推送传送, 而不会增加 HTML 的大小. 问题是, 服务器推送很麻烦 , 浏览器存在许多陷阱和竞争条件. 往往并不能始终支持, 且伴有一些缓存问题(参见 Hooman Beheshti 演示文稿幻灯片的 114 页). 事实上, 这种影响可能是负面的, 它会使网络缓冲区膨胀, 从而导致文档中真实帧的传递被阻止. 此外, 由于 TCP 启动缓慢, 服务器推送似乎在热连接上更有效.
即使使用 HTTP/1, 将关键 CSS 放在根域名下的单独文件中也是有好处的, 由于缓存的原因, 有时甚至比内联更优. Chrome 在请求页面时会尝试打开根域名下的第二个 HTTP 连接, 从而无需 TCP 连接来获取此 CSS(感谢 Philip!)
需要记住的一些问题是: 与可以从任何域触发预加载的 "预加载" 不同, 你只能从自己的域或认证过的域中推送资源. 一旦服务器从客户端获得了第一个请求, 就可以启动该连接. 服务器推送资源落在 Push 缓存中, 并在连接终止时被删除. 但是, 由于 HTTP/2 连接可以在多个选项卡中重复使用, 因此也可以使用通过其他选项卡的请求声明推送的资源(感谢 Inian!).
目前, 服务器没有简单的方法可以知道要推送资源是否已经存在于用户缓存之中 https://blog.yoav.ws/tale-of-four-caches/ 中, 每个用户访问的时候都会推送资源. 因此, 你可能需要创建 HTTP/2 的缓存感知服务器推送机制 https://css-tricks.com/cache-aware-server-push/ . 如果发现已存在, 则可以尝试根据缓存中已有内容的索引从缓存中获取它们, 从而避免服务器的全量推送.
但请记住, 新的 cache-digest 规范否定了手动构建此类 "缓存感知" 服务器的需要, 只需要在 HTTP/2 中声明一个新的帧类型, 就可以传达该域名下缓存中已有的内容. 因此, 它对 CDN 也特别有用.
对于动态内容, 当服务器需要一些时间来生成响应时, 浏览器无法发出任何请求, 因为它不知道页面可能引用的任何子资源. 对于这种情况, 我们可以预热连接并增加 TCP 拥塞窗口的数量, 以便可以更快地完成将来的请求. 此外, 所有内联资源通常都是服务器推送的良好候选者. 事实上, Inian Parameshwaran 针对 HTTP/2 推送与 HTTP 预加载做了很棒的比较的研究, 这份高质量的资料包括了你可能想了解的各种细节. 是否选择服务器推送? Colin Bendell 的我是否应该进行服务器推送? https://shouldipush.com/ 可能会为你指明方向.
一句话: 正如 Sam Saccone 所说, 预加载适用于将资源的开始下载时间向初始请求靠拢, 服务器推送适用于删除完整的 RTT(等 https://blog.yoav.ws/being_pushy/ , 具体取决于服务器的响应时间)- 前提是你得有一个 service worker 用来避免不必要的推送.
43. 尝试重组 CSS 规则
我们已经习惯了关键的 CSS, 但还有一些优化可以超越这一点. Harry Roberts 进行了一项非凡的研究, 得出了相当惊人的结果. 例如, 将主 CSS 文件拆分为单独的媒体查询可能是个好主意. 这样, 浏览器将检索具有高优先级的关键 CSS, 以及其他具有低优先级的所有内容 -- 最终完全脱离关键路径.
另外, 避免将 <link rel="stylesheet" /> 放在 async 标签之前. 如果脚本不依赖于样式表, 请考虑将阻塞脚本放在阻塞样式之前. 如果脚本依赖样式, 请将该 JavaScript 一分为二, 然后对应将其加载到 CSS 的前后.
Scott Jehl 通过使用 service worker 缓存内联 CSS 文件解决了另一个有趣的问题, 这是使用关键 CSS 时常见的问题. 基本上, 我们将 ID 属性添加到 style 元素中, 以便使用 JavaScript 时可以轻松找到它, 然后一小块 JavaScript 发现 CSS 并使用缓存 API 将其存储在本地浏览器缓存中(其内容类型为 text/CSS), 以便在后续页面中使用. 为了不在后续页面上内联引用, 而是从外部引用缓存的资源, 我们在第一次访问站点时设置了一个 cookie. 瞧!
YouTube 视频链接: https://youtu.be/Cjo9iq8k-bc
我们是否以流的方式进行响应了? 使用流, 在初始导航请求期间呈现的 HTML 可以充分利用浏览器的流 HTML 解析器.
44. 你有没有将请求设为 stream?
经常被遗忘和忽略的是 Streams https://streams.spec.whatwg.org/ 提供了一个读或写异步数据块的接口, 在任何给定的时间里, 内存中可能只有一部分数据块可用. 基本上, 它们允许发出原始请求的页面在第一块数据可用时立即开始处理响应, 并使用针对流优化的解析器逐步显示内容.
我们可以从多个来源创建一个流. 例如, 可以让 service worker 构造一个流, 其中 shell 来自缓存, 但主体来自网络, 而不是提供一个空的 UI shell 并让 JavaScript 填充它. 正如 Jeff Posnick 所说, 如果你的 Web 应用程序由 CMS 提供支持, 该 CMS 通过将部分模板缝合在一起呈现 HTML, 则可以将该模型直接转换为使用流响应, 模板逻辑将复制到 service worker 而不是你的服务器中. Jake Archibald 的 Web Streams 之年文章重点介绍了如何准确地构建它. 可以为性能带来相当明显的提升.
流式处理整个 HTML 响应的一个重要优点是, 在初始导航请求期间呈现的 HTML 可以充分利用浏览器的流式 HTML 解析器. 页面加载后插入到文档中的 HTML 块 (这在通过 JavaScript 填充的内容中很常见) 则无法享受这种优化.
浏览器支持怎么样呢? 主流浏览器, Chrome 52+,Firefox 57+,Safari 和 Edge 均支持该 API, 而所有的现代浏览器中都支持 https://caniuse.com/#search=serviceworker Service Workers.
45. 考虑使组件具有连接感知能力
随着不断增长的负载, 数据的开销可能变得很大 https://whatdoesmysitecost.com/ , 我们需要尊重选择在访问我们的网站或应用程序时希望节省流量的用户. Save-Data 客户端提示请求头允许我们为受成本和性能限制的用户定制应用程序及其负载. 事实上, 你可以将高 DPI 图像的请求重写为低 DPI 图像请求 https://css-tricks.com/help-users-save-data/ , 删除 Web 字体, 花哨的视差效果, 预览缩略图和无限滚动, 关闭视频自动播放, 服务器推送, 减少显示项目的数量并降低图像质量, 甚至改变交付标记的方式. Tim Vereecke 发表了一篇关于 data-s(h)aver 策略的非常详细的文章, 其中介绍了许多用于数据保存的选项.
目前, 只有 Chromium,Android 版本的 Chrome 或桌面设备上的 Data Saver 扩展才支持标识头. 最后, 你还可以使用 Network Information API 根据网络类型提供高 / 低分辨率的图像 和视频. Network Information API, 特别是 navigator.connection.effectiveType(Chrome62+)使用 RTT,downlink,effectiveType(以及一些其他值 https://wicg.github.io/netinfo/ )来为用户提供可处理的连接和数据表示.
在这种情况下, Max Stoiber 谈到连接感知组件 https://mxb.at/blog/connection-aware-components/ . 例如, 使用 React 时, 我们可以编写一个为不同连接类型呈现不同元素的组件. 正如 Max 建议的那样, 新闻文章中的 <Media /> 组件或许应该输出为下列的几种形式:
Offline: 带有 alt 文本的占位符,
2G / 省流 模式: 低分辨率图像,
非视网膜屏的 3G: 中等分辨率图像,
视网膜屏的 3G: 高分辨率视网膜图像,
4G: 高清视频.
DeanHume 提供了一个使用 service worker 的类似逻辑的实现. 对于视频, 我们可以在默认情况下显示视频海报, 然后显示 "播放" 图标, 在网络更好的情况下显示视频播放器外壳, 视频元数据等. 作为浏览器不兼容的降级方案, 我们可以监听 canplaythrough 事件, 并在 canplaythrough 事件 2 秒内未触发的情况下使用 Promise.race() 来触发资源加载超时.
46. 考虑使组件具有设备内存感知能力
尽管如此, 网络连接也只是为我们提供了关于用户上下文的一个视角. 更进一步, 你还可以动态地根据可用设备内存调整资源, 使用 Device Memory API(Chrome63+).navigator.deviceMemory 返回设备的 RAM 容量(以 GB 为单位), 四舍五入到最近的 2 次方. 该 API 还具有客户端提示标头 Device-Memory, 该标头可以提供相同的值.
.
来源: https://juejin.im/post/5c60ed6cf265da2dd4274724