本文为《前端性能优化》阅读笔记。

一、缓存

如果远程请求都不必发出,又何须优化加载性能呢?
所以,审视一下我们的应用、业务,看看哪些性能问题是可以在源头上解决的。
疑问:什么时候使用cache api,什么时候使用http缓存?
答:相较于http缓存,cache api可以纯前端掌控是否保存缓存,无需添加额外的http首部字段,利用Service Worker代理请求,挑选想要缓存的服务数据。

1、本地数据存储

  • 对于一些请求,我们可以直接在业务代码侧进行缓存处理。缓存方式包括 localStorage、sessionStorage、indexedDB。
  • 适用场景:日更新榜单、用户身份信息

    2、内存缓存

  • 适用场景:当你访问一个页面及其子资源时,有时候会出现一个资源被使用多次,例如图标。

  • 浏览器帮助我们实现的优化,感知不到。

    3、cache api

  • 提供给了客户端构建请求缓存机制的能力。

  • Cache 接口为缓存的 Request / Response 对象对提供存储机制,例如,作为ServiceWorker 生命周期的一部分,Cache 接口像 workers 一样,是暴露在 window 作用域下的。
  • 通常和Service Worker配合使用。
  • Service Worker有一个非常重要的特性:你可以在Service Worker中监听所有客户端(Web)发出的请求,然后通过Service Worker来代理,向后端服务发起请求。通过监听用户请求信息,Service Worker可以决定是否使用缓存来作为Web请求的返回。image.png
  • 想要进一步了解 Service Worker 和 Cache API 的使用可以看这篇文章[3]。同时推荐使用 Google 的 Workbox

    4、http缓存

  • 强缓存不发请求到服务器,协商缓存会发请求到服务器。

    • 1)浏览器在加载资源时,根据请求头的expires和cache-control判断是否命中强缓存,是则直接从缓存读取资源,不会发请求到服务器。
    • 2)如果没有命中强缓存,浏览器一定会发送一个请求到服务器,通过last-modified和etag验证资源是否命中协商缓存,如果命中,服务器会将这个请求返回,但是不会返回这个资源的数据,依然是从缓存中读取资源
    • 3)如果前面两者都没有命中,直接从服务器加载资源

      5、push cache

  • HTTP/2 的 Push 功能所带来的

  • 不会在额外检查资源是否过期
  • 存活时间很短,甚至短过内存缓存(例如有文章提到,Chrome 中为 5min 左右);
  • 只会被使用一次;
  • 服务端“推送”的一些其他资源

    二、请求

    1、避免多余重定向
    2、DNS预解析

  • 访问远程服务的时候,不会直接使用服务的出口 IP,而是使用域名。

    • 直接使用服务出口IP的恐怕是异端……
  • DNS 解析流程可能会很长,耗时很高,所以整个 DNS 服务,包括客户端都会有缓存机制
  • dns-prefetch

    1. <link rel="dns-prefetch" href="//yourwebsite.com">

    3、预先建立链接
    浏览器可以视情况完成部分工作。

    1. <link rel="preconnect" href="//sample.com">

    4、使用CDN
    对于使用 CDN 的资源,DNS 解析会将 CDN 资源的域名解析到 CDN 服务的负载均衡器上
    前提是使用了DNS解析。

    三、服务端响应

    四、页面解析与处理

  • 解析js时使用defer 或 async属性

    • defer 会在 HTML 解析完成后;而 async 则是下载完成就立即开始执行
    • 推荐在一些与主业务无关的 JavaScript 脚本上使用 async。例如统计脚本、监控脚本、广告脚本等。
  • HTML 的文档大小也会极大影响响应体下载的时间。

    五、静态页面资源

    javascript

    1、减少不必要的请求

  • 代码拆分&按需加载

    • 主要目的是按需加载,代码拆分只是为按需加载服务
  • 代码合并

    • 不对 node_modules 中的代码进行打包合并,那么当我们请求一个脚本之前将可能会并发请求数十甚至上百个依赖的脚本库。
    • 千万不要让你的碎文件散落一地。
    • 疑问:代码拆分与代码合并看起来是很矛盾的存在,如果不拆分代码,请求的文件过大会请求很久;如果拆分代码,又会导致请求并发?

      2、减少包体大小

  • 使用 UglifyJS 做源码级别的压缩

    • 将变量替换为短命名、去掉多余的换行符等方式。
    • 基本已经成为了前端开发的标配。
  • 一些文本压缩算法
    • gzip
    • 在响应头Content-Encoding中会体现
    • 深色的数字表示压缩后的大小为 22.0KB,浅色部分表示压缩前的大小为 91.9KBimage.png
    • 在 Nginx 中就包含了 ngx_http_gzip_module[3] 模块,通过简单的配置就可以开启
  • Tree-Shaking
    • Tree Shaking 最早进入到前端的视线主要是因为 Rollup,后来在 webpack 中也被实现了
    • 本质是通过检测源码中不会被使用到的部分,将其删除,从而减小代码的体积。
    • Tree Shaking 非常依赖于 ESM,都属于静态分析
      • 前端流行的工具库 lodash 一般直接安装的版本是非 ESM 的,为了支持 Tree Shaking,我们需要去安装它的 ESM 版本 —— lodash-es 来实现 Tree Shaking[6]。
  • 优化polyfill 的使用
    • 一些程序员们开发了新特性对应的 polyfill,用于在非兼容浏览器上也能使用新特性的 API。
    • 只加载真正所需的 polyfill 将会帮助你减小代码体积
  • webpack

    • 可以通过 webpack-bundle-analyzer 这个工具来查看打包代码里面各个模块的占用大小
    • 打包体积过大主要是因为引入了不合适的包

      3、解析与执行

  • javascript解析耗时

    • 删除不必要的代码,对于降低 Parse 与 Compile 的负载也是很有帮助的
  • 避免long task(耗时长的javascript运行)
  • 做工具的主人而不是奴隶,衡量业务场景是否真的需要框架

    4、缓存

    以下方案均基于一个前提讨论:新项目上线部署。小公司里的新项目上线部署改动大,没有做任何增量部署的考虑,属于完全没接触过的盲区。

  • 将基础库代码打包合并

    • 不会每次发布新版都会让用户花费不必要的带宽重新下载基础库。
  • webapack optimization.splitChunks分离公共库
  • 减少webpack编译不当带来的缓存失效

    • 自增id与hash id
    • runtimeChunk
    • records

      CSS

      老生常谈

  • 按需加载、请求合并、优先级

  • 代码压缩:CSS uglify 工具,例如 clean-css,可以优化代码、删除多余的换行与空格。
  • polyfill:选择合适的兼容性

    编码优化

  • 避免使用昂贵的属性

  • flex 进行布局比我们用的一些“老式”方法性能更好
  • 慎用@import:这会把你的请求变得串行化
  • 简化选择器

    • 样式数据是一个超大的结构,为每一个元素查找匹配的规则会造成性能问题,同时,复杂的层叠规则也会带来很高的复杂度。
    • 使用 SASS、LESS 这样的工具时,避免过多的嵌套。

      利用缓存

  • 如果使用 webpack 作为构建工具,我们一般会使用 css-loader 和 style-loader,这样可以直接在 JavaScript 代码中 import 样式文件。不过这样带来的一个问题就是样式代码其实是耦合在 JavaScript 代码中的,通过运行时添加 style 标签注入页面。

  • 一个更好的做法是在生产环境中将样式信息单独抽离成 CSS 文件

    图片

    1、优化请求数

  • 雪碧图

    • 请求多个图片时,可能受到浏览器并发 HTTP 请求数的限制
    • 将图标合并为一张大图可以实现「20+ → 1」的巨大缩减
    • 在 webpack 中使用 webpack-spritesmith,或者在 gulp 中使用 gulp.spritesmith
  • 懒加载

    • 尽量只加载用户正在浏览或者即将会浏览到的图片
    • 可以借助一些已有的工具库,例如 aFarkas/lazysizesverlok/lazyloadtuupola/lazyload
    • 在 CSS 中使用的图片一样可以懒加载url()
    • 将图片原 url 的值替换为 base64
      • 常用于首屏加载 CRP 或者骨架图上的一些小图标

        2、减小图片大小

  • 在网站上使用 WebP 格式

  • 一些需要缩放与高保真的情况,或者用作图标的场景下,使用 SVG
  • 使用静音(muted)的 video 来代替 GIF
  • 使用合适的大小和分辨率
  • 质量权衡
  • 删除冗余的图片信息
  • SVG压缩

    3、缓存

    六、运行时

    1、强制同步布局

    应该尽量避免直接通过javascript操作DOM,走react的渲染能更好地提升性能,因为react有一个diffing机制,会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使 DOM 达到预期的状态,实现了一个局部更新的效果。而其setState的更新队列机制,也是一个批量更新的过程。

    2、长列表优化

  • virtual list:只渲染可见区域附近的列表元素。

  • 更推荐参考开源实现,比如react-window

    3、避免javascript运行时间过长

  • 运行栈中的javascript任务会占用其他任务导致卡顿

  • 解决方案

    • 任务分解
    • 延迟执行
    • 并行计算:web worker

      4、善用Composite

  • 动画相关

  • 渲染合成层
  • GPU硬件加速

    5、滚动事件的性能优化

  • 防抖和节流

    6. Passive event listeners

  • Passive event listeners用于阻止浏览器默认行为

    其他:监控与测试

  • 一般会把性能数据分为两种

    • Lab data,例如在 CI/CD 中加入 lighthouse。
    • Field data,也被成为 RUM (Real User Monitoring),是指采集线上实际的性能数据来进行监控。
  • 标准化场景建议用lab data;复杂场景建议用field data