资源大小

  • 压缩
    • Gzip
    • br(Brotli)
  • JS Uglify,minify
    • 使用 Tree Shaking 排除没有使用的导入模块
    • webpack-bundle-analyzer
    • webpack-libs-optimization
  • 图像优化
    • WebP
  • 字体,视频优化
  • JS 分包
    • 大于 30KB 的 async/defer 文件可以触发 V8 Script Streaming
    • 有利于 HTTP2 的多路复用

加载时序

  • PRPL 模式
    • Push (or preload) the most important resources(首屏加载前)
    • Render the initial route as soon as possible(首屏加载)
    • Pre-cache remaining assets(为下一个场景做准备)
    • Lazy load other routes and non-critical assets(不是关键的资源延迟加载)
  • 活用 async script,尽量异步加载非首次渲染需要的资源
  • 活用 defer script,提前 first paint 的时间点
  • 缓存策略
  • 优化入口文件大小
    • TCP 的 first roundtrip 只能发 10 个 TCP packets(大概是14KB)
  • 使用 <link rel="preload" /><link rel="dns-prefetch" >
    • preload-webpack-plugin
  • 首屏渲染需要外部 JS,CSS,应该尽可能的放到 HTML 文档上方(Header)以便尽早发出请求
    • CSS 会阻塞 JS,所以 CSS 应该放在更前面
  • 这里 介绍了一些到 2018 年为止 CSS 应该怎么放置的策略

渲染逻辑

  • 避免不必要的重排,重绘
    • 慎重使用会产生重排,重绘的方法,具体参见 CSS Triggers
    • 避免强制同步布局(修改后马上查询), 浏览器本不需要在每次查询的时候就马上就去重排的
  • 避免布局抖动(循环内反复获取和修改)
    • 可以使用 FastDOM 批处理 DOM 的读取和写入
  • 可以单独创建新的合成器层(但是不要创建太多了, 耗内存)
    • will-change 可以做到, 并提前警示浏览器即将出现更改
    • transform: translateZ(0) 在旧浏览器里也可以做到
  • Flexbox 要好于浮动布局
  • 避免复杂的 CSS 选择器
    • :nth-last-child 这种要慎用

JS 逻辑

  • 对于大型任务
    • 特别大的,没有 DOM 操作的,可以考虑 Web Worker
    • 活用几个异步的回调函数将大型任务分割
      • requestAnimationFrame 保证 JavaScript 在帧开始时(Safari 为帧结束)运行,这个对于实现动画效果很有帮助(cocos 的每一帧都是在 RAF 回调内的)
      • requestIdleCallback 在浏览器空闲的时候执行(貌似 safari 还不支持)
  • 慎用微优化(忽略 JS 方法间的性能差距,因为大部分时候他们微乎其微)
  • 活用防抖动和节流阀
    • 防抖:Debounce 触发事件后 n 秒内函数只会执行一次, 如果 n 秒内事件再次被触发则重新计时

      • 用于搜索联想词,用户的频繁点赞操作,resize(只执行最后一次就可以了)
      • debounce.js · GitHub
      const debounce = function(fn, idle) {
        let last;
        return function() {
          // 每次触发事件时都取消之前的延时调用方法
          clearTimeout(last);
          last = setTimeout(() => {
            fn.apply(this, arguments);
          }, idle)
        };
      }
    • 节流: throttle 在 n 秒内只会执行一次,若果有多次则忽略后面的
      • 类似 RAF
      • 可以用于 loadmore 的实现
      const throttle = function(fn, delay) {
        let timer = null;
        return function() {
      	// 每次触发事件时都判断当前是否有等待执行的延时函数, 如果有则不执行
      	if(!timer) {
      	  timer = setTimeout(() => {
      		fn.apply(this, arguments);
      		timer = null;
      	  }, delay);
      	}
        };
      }

工程角度

  • 为不同的环境配置不同的策略
    • navigator.connection.effectiveType 可以更准确的检测当前网络环境,Chrome 62 开始支持

汇总建议

浏览器加载过程

缓存策略

检查 Cache-Control 的时间或者是 Expires 的时间戳,如果满足,则使用本地的缓存 根据上一次服务端发送的 ETag 发送 If-None-Match 给服务端或者是根据上一次的 Last-modified 发送 If-Modified-since 给服务端,服务端再根据这个判断是否要返回 304 给客户端,如果客户端收到 304,则使用上一次的缓存信息 同时也可以用 Service Worker 进行离线化操作

传输

如果是 http 检查 HSTS Preload 列表, 决定是否需要从 http 转换到 https 把 URL 通过 DNS 解析到服务器的真正IP 可以在 script 标签里设置 dns-prefetch 来缓存 DNS 的解析结果,这样下次真的要请求的时候就不需要再进行 DNS 解析了 通过 TCP 三次握手建立连接 建立连接成功后要考虑 TCP 为了防止网络拥塞采取了慢启动策略,所以若果可以的话, 第一个 HTML 文件建议小于 14KB(10 个 TCP packets)

渲染

解析 HTML 文档,构建 DOM 树,然后下载 CSS 资源,构建 CSSOM 将 DOM 和 CSSOM 合并后进行排版,绘制(layout,paint,紫色和绿色) 这里需要关注的是 HTML 内 script 的 async 和 defer 标签,async 代表可以一步加载,但是加载完后会立即开始执行,而 defer 会等到 HTML 解析后再执行

2019 前端性能优化年度总结 by Vitaly Friedman Web performance made easy (Google I/O ‘18)