前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

前端页面性能优化实战,从测量到加载渲染全链路调优

首页2021-02-08 10:50:03Front-End
性能优化前端性能LighthouseWebpack懒加载缓存

页面慢这件事最难受的地方,不是不知道怎么优化,是不知道该优化哪儿。有人上来就压图片,有人先去拆包,忙活一通首屏时间纹丝不动。我自己也走过这段弯路,后来才想明白,性能优化的第一步从来不是动手改代码,是先把「慢在哪一毫秒」量出来。

这篇按这个顺序展开:先把测量工具和 Web API 讲清楚,让你能拿到数字;再按加载、渲染、缓存三条线逐条给可落地的手段,压缩、拆包、骨架屏、虚拟列表、预加载懒加载都在里面。每一条都说清它在解决哪个指标,以及什么场景下用了也白用。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • Network、Lighthouse、Performance、WebPageTest 各自适合看什么
  • 用 Performance API 自己算白屏、TTI、DNS 这些指标
  • 雅虎军规里今天还真正管用的那几条
  • Gzip、服务端压缩、JS/CSS/HTML 压缩、HTTP/2 首部压缩的实际收益
  • webpack 侧的 DllPlugin 与 splitChunks 拆包
  • 骨架屏、虚拟列表、白屏 loading 这类体验优化的原理
  • HTTP 缓存四件套和 Service Worker
  • preload、prefetch 和图片、路由懒加载的正确用法

# 一、先把问题量出来,四类调试工具

工欲善其事必先利其器。这一节的工具不用全学,但至少要知道各自看什么。

# 1.1 Network

Chrome DevTools Network 面板总览

Network 面板能看到资源加载详情,用来初步评估影响页面性能的因素。鼠标右键可以自定义列,页面底部是当前加载资源的一个概览。里面有两个数字要先认准:DOMContentLoaded 是 DOM 渲染完成的时间,Load 是当前页面所有资源加载完成的时间。

先想一个问题,怎么判断哪些资源对当前页面加载是无用的?

答案不在这个面板里。按 shift + cmd + P 调出命令面板,输入 Coverage 打开覆盖率工具,它会告诉你每个 JS 和 CSS 文件里有多少字节是本次访问根本没执行到的:

DevTools 命令面板与扩展工具

同一个命令面板里还能调出 Performance monitor,实时看 DOM 节点数、JS 堆大小、每秒布局次数的变化:

Performance monitor 实时监控页面性能

DOM 节点数持续上涨而不回落,基本可以断定有泄漏,这个指标比内存曲线更直观。

# 瀑布流 waterfall

单条请求点开的这个时间条,是排查慢请求最重要的信息:

Network 瀑布流各阶段耗时拆解

  • Queueing 浏览器将资源放入队列的时间
  • Stalled 因放入队列时间而发生的停滞时间
  • DNS Lookup DNS 解析时间
  • Initial connection 建立 HTTP 连接的时间
  • SSL 浏览器与服务器建立安全性连接的时间
  • TTFB 等待服务端返回数据的时间
  • Content Download 浏览器下载资源的时间

这七段的意义在于它能直接告诉你锅在哪边。TTFB 长是后端慢或者网络链路远,Content Download 长是资源太大,Queueing 和 Stalled 长通常是同域名并发请求数打满了。三种情况的解法完全不同,光看一个总耗时是分不出来的。

# 1.2 Lighthouse

Lighthouse 性能评分报告

Lighthouse 会根据 Chrome 的一套策略自动对网站做质量评估,并给出优化建议。几个核心指标:

  • First Contentful Paint 首屏渲染时间,1s 以内是绿色
  • Speed Index 速度指数,4s 以内是绿色
  • Time to Interactive 到页面可交互的时间

它最大的价值是把「感觉有点慢」变成一个可以拿去开会的分数,而且给的建议基本都能直接照着做。缺点是本地跑分受你自己的机器和网络影响很大,同一个页面开着一堆标签页跑和干净环境跑能差二十分,看趋势别看绝对值。

# 1.3 Performance

Performance 面板的火焰图分析

Performance 是对网站最专业的分析面板,能看到每一帧里 JS 执行、样式计算、布局、绘制各花了多久。前面两个工具告诉你有问题,这个告诉你问题具体在哪一行代码。

学会读火焰图之后,很多玄学问题会立刻变得具体。渲染链路这块我在 浏览器渲染路径与性能优化 里单独展开过,可以配着看。

# 1.4 WebPageTest

WebPageTest 可以模拟不同场景下的访问情况,比如不同浏览器、不同国家、不同网速,在线测试地址是 webPageTest。

WebPageTest 测试结果概览

WebPageTest 逐帧加载视图

它比本地 Lighthouse 强的地方是节点在全球各地,能真实反映海外用户或者弱网用户看到的样子。做出海业务的话这个工具比什么都实在。

# 1.5 打包体积分析

前面几个看的是运行时,这个看的是产物。

webpack-bundle-analyzer 会把打包结果画成一张矩形树图,哪个包占地大一目了然:

webpack-bundle-analyzer 体积树图

安装和配置:

npm install --save-dev webpack-bundle-analyzer
@前端进阶之旅: 代码已经复制到剪贴板
// webpack.config.js 文件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports={
  plugins: [
    new BundleAnalyzerPlugin({
          analyzerMode: 'server',
          analyzerHost: '127.0.0.1',
          analyzerPort: 8889,
          reportFilename: 'report.html',
          defaultSizes: 'parsed',
          openAnalyzer: true,
          generateStatsFile: false,
          statsFilename: 'stats.json',
          statsOptions: null,
          logLevel: 'info'
        }),
  ]
}
@前端进阶之旅: 代码已经复制到剪贴板

再在 package.json 里加个入口:

"analyz": "NODE_ENV=production npm_config_report=true npm run build"
@前端进阶之旅: 代码已经复制到剪贴板

另一条路是开 source-map 配合 source-map-explorer,它的粒度更细,能看到某个第三方库里具体是哪几个文件占了体积。先在 webpack.config.js 里开 source-map:

module.exports = {
    mode: 'production',
    devtool: 'hidden-source-map',
}
@前端进阶之旅: 代码已经复制到剪贴板

hidden-source-map 会生成 map 文件但不在 bundle 末尾写引用注释,这样浏览器 DevTools 不会自动加载它,源码不至于泄漏出去,同时本地分析工具还能用。

然后配脚本:

"analyze": "source-map-explorer 'build/*.js'",
@前端进阶之旅: 代码已经复制到剪贴板

跑 npm run analyze 出来的结果长这样:

source-map-explorer 分析结果

我一般是先用 webpack-bundle-analyzer 找大块,锁定某个库之后再用 source-map-explorer 看内部构成,判断是能 tree-shaking 掉还是得整个换掉。

# 二、用 Web API 自己采数据

工具面板只能看当下这一次访问。要做长期监控,得靠浏览器提供的这几个 API 自己上报。

# 2.1 监听视窗激活状态

页面可见性变化的效果演示

用户切走标签页之后,页面里的定时器、轮询、动画其实都还在跑,白白烧电和流量。visibilitychange 就是用来管这件事的:

// 窗口激活状态监听
let vEvent = 'visibilitychange';
if (document.webkitHidden != undefined) {
    vEvent = 'webkitvisibilitychange';
}

function visibilityChanged() {
    if (document.hidden || document.webkitHidden) {
        document.title = '客官,别走啊~'
        console.log("Web page is hidden.")
    } else {
        document.title = '客官,你又回来了呢~'
        console.log("Web page is visible.")
    }
}

document.addEventListener(vEvent, visibilityChanged, false);
@前端进阶之旅: 代码已经复制到剪贴板

这段里的 webkitHidden 是老 Safari 和老 Android 浏览器时代的前缀写法,现在主流浏览器全都支持无前缀的 document.hidden 和 visibilitychange,新项目直接用标准写法就够了,不用再做这层降级判断。

真正有价值的用法不是改标题逗用户,是在 hidden 时暂停轮询、停掉 requestAnimationFrame、断开 WebSocket,回到 visible 时再恢复。做直播或者行情类页面,这一下能省掉相当可观的资源。

# 2.2 观察长任务

主线程上超过 50ms 的任务就算长任务,它是页面卡顿、点击没反应的直接原因。PerformanceObserver 能把它们全捞出来:

const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        console.log(entry)
    }
})

observer.observe({entryTypes: ['longtask']})
@前端进阶之旅: 代码已经复制到剪贴板

拿到 entry 之后上报到监控平台,就能知道线上用户实际卡在哪些操作上,比在本地反复复现靠谱得多。

# 2.3 监听网络变化

网络变化时给用户一个反馈,看直播的时候网络卡了平台会提醒你或者自动降清晰度,就是这套东西:

var connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
var type = connection.effectiveType;

function updateConnectionStatus() {
  console.log("Connection type changed from " + type + " to " + connection.effectiveType);
  type = connection.effectiveType;
}

connection.addEventListener('change', updateConnectionStatus);
@前端进阶之旅: 代码已经复制到剪贴板

这个我踩过一个坑,navigator.connection 的兼容性比看起来差,Safari 到现在都不支持,直接取 connection.effectiveType 会抛错。上生产一定要判空,别学上面这段直接往下写。

# 2.4 计算 DOMContentLoaded 时间

window.addEventListener('DOMContentLoaded', (event) => {
    let timing = performance.getEntriesByType('navigation')[0];
    console.log(timing.domInteractive);
    console.log(timing.fetchStart);
    let diff = timing.domInteractive - timing.fetchStart;
    console.log("TTI: " + diff);
})
@前端进阶之旅: 代码已经复制到剪贴板
fe
  • 一、先把问题量出来,四类调试工具
    • 1.1 Network
      • 瀑布流 waterfall
    • 1.2 Lighthouse
    • 1.3 Performance
    • 1.4 WebPageTest
    • 1.5 打包体积分析
  • 二、用 Web API 自己采数据
    • 2.1 监听视窗激活状态
    • 2.2 观察长任务
    • 2.3 监听网络变化
    • 2.4 计算 DOMContentLoaded 时间
    • 2.5 更多计算规则
  • 三、雅虎军规里今天还管用的几条
    • 3.1 减少 cookie 传输
    • 3.2 避免过多的回流与重绘
  • 四、压缩
    • 4.1 Gzip
    • 4.2 服务端压缩
    • 4.3 JavaScript、CSS、HTML 压缩
    • 4.4 HTTP/2 首部压缩
  • 五、webpack 侧的构建优化
    • 5.1 DllPlugin 提升构建速度
    • 5.2 splitChunks 拆包
  • 六、骨架屏
  • 七、窗口化
  • 八、缓存
    • 8.1 HTTP 缓存
    • 8.2 Service Worker
  • 九、预加载与懒加载
    • 9.1 preload
    • 9.2 prefetch
    • 9.3 图片懒加载
    • 9.4 路由懒加载
  • 十、SSR 与 react-snap
  • 十一、体验优化
    • 白屏 loading
  • 总结
  • 参考

← 异步编程入门之RxJs 从宝石图到常用操作符浅析Nginx之try_files指令与SPA路由回退 →