前端web项目性能优化

7 阅读36分钟

性能指标都有哪些?分别是什么?下面是我之前总结的一些指标内容

  • FCP

    • 解释:FCP(First Contentful Paint ),中文翻译过来时首次内容绘制,它的衡量标准是从网页开始加载到用户能看到第一个内容元素截止。

    • 意义

      • 表示感知加载速度,即网页可以多快地加载网页中的所有视觉元素并将其渲染到屏幕上
      • FCP快可以让用户确信正在发生的事情。这个有点绕,我是这样理解的,比如一个用户访问网站A,网站A的完全加载渲染时间是10秒,如果说中间我们一直白屏等待,用户10秒的感知都是页面没有变化,从而产生疑惑(页面到底有没有在加载?是不是我电脑出问题了?)。如果说在这10秒,每加载一个元素,渲染一次,给人类视觉产生更新,从而用户会觉得(这10秒网页确实在加载,只是资源多,所以加载慢)
  • LCP

    • 解释:Largest Contentful Paint (LCP)最大内容绘制,它是核心网络特征指标,表达的是网页从开始加载到主要内容元素被绘制的时间。

    • 意义:

      • 可以衡量感知的加载速度。它来自与web.dev,我对它的理解是 用户对一个网页加载速度的感觉(人对页面完全加载的感知时间)
      • 快速的LCP可以提升用户对网页视觉等待体验。
  • FID:

    • 解释:First Input Delay (FID)首次可交互延时,是衡量用户首次与网页互动(即,点击链接、点按按钮或使用由 JavaScript 提供支持的自定义控件)到浏览器实际能够开始处理事件处理脚本以响应相应互动的时间。前文提到的FCP、LCP都是旨在视觉上的体验,而FID对用户在交互上体验的一种衡量方式。

    • 意义:

      • 可以衡量用户对网站的互动性和响应速度的第一印象
      • 快速的FID可以让用户保持一个对网站良好的交互印象
  • TTI:

    • 解释:Time to Interactive(TTI)可交互时间,衡量从网页开始加载到其主要子资源加载完成所用的时间,并且能够快速可靠地响应用户输入。可以衡量加载响应能力。

    • 需要注意的是:

      • 该指标是实验指标,变化比较敏感(来着离群网络请求和耗时较长的任务),所以变化比较大
      • 该指标目前已经从Lighthouse 10 中移除,取而代之可以用Largest Contentful Paint (LCP)、Total Blocking Time (TBT) 和 Interaction to Next Paint (INP) 等较新的替代指标通常更适合用来取代 TTI。
    • 测定规则:

      1. 先锁定起点:首次内容绘制(FCP)完成;
      2. 再找合格窗口:从 FCP 后寻找一个 **≥5s 的静默窗口 **—— 窗口内无超过 50ms 的长任务(Long Task,浏览器主线程单次执行超过 50ms 的任务,会阻塞用户输入响应),且窗口内正在进行的网络请求≤2 个;
      3. 最终取窗口起点:这个静默窗口的起始时间,就是最终的 TTI。如果未找到长任务,则与 FCP 值相同)。
  • TBT:

    • 解释:

      • Total Blocking Time(TBT)总阻塞时间,衡量在首次内容绘制 (FCP) 之后,主线程处于阻塞足够长的时间以防止输入响应所用的总时长。
      • 上面那句话特别绕,其实我感觉就是在FCP之后,主线程被阻塞的总时间,如果时间很长,相当于FCP之后的很长一段时间内,页面都是不可交互的。

    需要注意的是:TBT是一个实验室指标


团队内部 TTI 计算 SDK 的实现与发布流程

1. 核心依赖选型

完全基于Google 官方****web-vitals(体积 < 2KB,无额外依赖,现代浏览器全兼容,自动处理降级场景)—— 它是 Lighthouse、Chrome DevTools 的同源实现,计算规则 100% 一致,从根源上避免自定义逻辑的边界处理遗漏、口径偏差。

2. 核心计算流程(SDK 内部封装,对外仅暴露initTTICore入口)

特殊处理:

  • 防止重复上报在 SDK 内部维护全局的路由 TTI 采集状态,用户切换路由时,先中断上一次未完成的采集逻辑重置所有状态,再触发防抖,仅当用户停止切换(防抖超时)后,才开始采集最终目标路由的 TTI。
  • 增加「页面卸载」监听,防止内存泄漏:若用户在路由 TTI 采集过程中关闭标签页 / 跳转到其他域名,SDK 会监听beforeunload事件,强制重置状态,避免检测器 / 定时器挂起导致内存泄漏:
  • 过滤无效数据(页面加载超过20s的极端异常值)
// 伪代码简化版,突出核心逻辑
import { onTTI, type TTIMetric } from 'web-vitals';

export function initTTICore(config) {
  // 1. 前置过滤:采样率控制(大流量可下调,避免后端压力)、无效环境过滤(如页面后台挂起)
  if (Math.random() > config.sampleRate) return;

  // 2. 核心调用:web-vitals的onTTI回调,自动完成标准计算
  onTTI((metric: TTIMetric) => {
    // 3. 二次过滤:SDK内部再做一层业务场景的无效数据过滤(如页面加载超过60s的极端异常值)
    if (!isPageValid()) return;

    // 4. 组装上报数据:TTI核心值、关联因果指标(FCP/TBT,用于根因分析)、用户环境信息(用于TP90分维度统计)
    const reportParams = {
      metricType: 'tti',
      value: metric.value, // 核心TTI值,单位ms
      rating: metric.rating, // 官方评级:good/needs-improvement/poor
      fcp: 0,
      tbt: 0,
      env: getEnvInfo(),
      // ...其他业务字段
    };

    // 5. 上报数据:优先用sendBeacon(不阻塞主线程,页面卸载也能完成上报),降级用fetch+keepalive
    reportData(reportParams);
  });
}

(二)SDK 的发布流程(滴滴内部私有 npm,严禁发布公网)

1. 前置准备

  • 配置内部 npm 源:在项目根目录创建.npmrc,指定滴滴内部 npm 源(如registry=https://npm.didi.cn/);
  • 申请发布权限:向公司 npm 管理员申请@didi组织下的子包发布权限(如@didi/tianji-tti-monitor-sdk);
  • 本地全流程验证:在测试环境验证「采集→过滤→上报→数据统计」全链路正常,确认无性能影响、无 JS 错误。

2. 打包配置(用 Rollup,专为 SDK 开发设计,体积小无冗余)

// rollup.config.js 核心配置importresolvefrom'@rollup/plugin-node-resolve';importcommonjsfrom'@rollup/plugin-commonjs';importtypescriptfrom'@rollup/plugin-typescript';importterserfrom'@rollup/plugin-terser';exportdefault{input:'src/index.ts',output:[// ES Module产物,适配Umi等现代框架{file:'dist/index.esm.js',format:'esm',sourcemap:true},// CommonJS产物,兼容老版本环境{file:'dist/index.cjs.js',format:'cjs',sourcemap:true},// UMD压缩产物,支持script标签直接引入{file:'dist/index.umd.min.js',format:'umd',name:'TianjiTTIMonitor',plugins:[terser()],sourcemap:true},],plugins:[resolve(),commonjs(),typescript({declaration:true,declarationDir:'dist'})],// 外部依赖不打包,减少SDK体积external:['web-vitals'],};

3. 版本管理规范(严格遵循语义化 SemVer,面试加分)

  • 主版本号**major**:不兼容的 API 变更(1.0.0 → 2.0.0);
  • 次版本号**minor**:向下兼容的功能新增(1.0.0 → 1.1.0);
  • 补丁版本号**patch**:向下兼容的 bug 修复(1.0.0 → 1.0.1);
  • 测试版本:正式发布前先打 beta 版(1.0.0-beta.1),用于测试环境验证。

4. 完整发布步骤

  1. 执行npm run build,生成生产环境产物;
  2. 执行npm version 版本号,更新版本并生成 git tag;
  3. 执行npm publish,发布到内部 npm 源(beta 版加--tag beta);
  4. 执行git push origin --tags,推送 tag 到 git 仓库,方便版本回溯。

5. 灰度发布策略(避免影响全站用户)

  • 在测试环境验证 2-3 天,确认无问题;
  • 发布正式版,先在天机 1 个非核心子模块灰度验证 1 天,确认无性能影响、上报正常;
  • 全量推广到主应用和所有子代码库。

TTI优化的难点与过程?

难点一、团队内无现成方案&排期紧张

  1. 无现成优化方案可复用,需从零到一分析定位:团队此前未做过系统性的性能优化,没有成熟的配置模板和落地流程,所有问题都需要自己通过工具量化分析、逐一定位,且要兼顾“性能提升”和“不影响现有业务”;
  2. 排期紧张,要求快速落地且见效果:部分复杂页面业务侧反馈卡顿问题,团队只给了一个月排期,需要在这期间完成「分析定位-方案选型-优先级排序-开发调试-线上验证」全流程,时间紧、任务重,对方案的合理性和落地效率要求极高。
  3. 刚开始思路比较乱,不知道从哪里下手比较好

怎么去解决:

基于以上的两个背景,我首先确定了“先量化分析,再按成本-收益排优先级落地”的核心思路,先定位具体的性能瓶颈,锁定“加载阶段耗时占比70%、JS执行时间过长”的核心问题;再用webpack-bundle-analyzer拆解打包产物,定位出主包加载时间过长体积大、工具库全量引入、公共组件重复打包、静态资源无优化四大瓶颈。

基于定位结果,我梳理出4套优化方案,从实施成本、性能收益、线上风险三个维度评估,确定了从易到难、收益从高到低的优化优先级:

  1. webpack分包+公共依赖抽离+treeshaking 删除冗余代码(优先级最高):调整splitChunks配置抽离公共依赖,虽需调试配置,但长期收益最高,调整完后包体积减少约40.57%
  2. 静态资源优化(优先级第二):图片webp压缩+CDN+强缓存配置,开发量最小、无业务风险,收益立竿见影;
  3. 工具库按需引入(优先级第三):通过babel-plugin-import配置Antd、Lodash按需加载,仅修改配置文件,风险低,能大幅缩减包体积;
  4. 首屏埋点治理(优先级第四):无用omega埋点请求删除,对必须的首屏曝光,omega合并成 1 个批量上报
  5. 路由懒加载(优先级第五):基于Umi动态路由+React.lazy+Suspense做懒加载+兜底,少量修改路由配置,无核心业务影响;
  6. 代码细粒度优化(优先级较低):(接口拆分、使用性能优化hooks、)。
优化类别具体措施优先级见效速度备注
包体积优化精细化分包⭐⭐⭐⭐⭐⭐⭐⭐⭐首屏加载可减少1.5MB+,需调整Webpack配置
treeshaking 删除冗余代码⭐⭐⭐⭐⭐⭐⭐⭐
大的工具库平替⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐Moment.js → Day.js Lodash → Lodash-es + Tree Shaking
压缩插件优化删除⭐⭐⭐⭐⭐⭐⭐⭐⭐
按需加载⭐⭐⭐⭐⭐⭐⭐⭐
图片以及静态资源压缩⭐⭐⭐⭐⭐
渲染优化图片懒加载(Intersection Observer)⭐⭐⭐⭐⭐⭐⭐⭐减少首屏请求数,兼容性良好
内联关键、异步加载、压缩⭐⭐⭐⭐⭐⭐⭐
关键图片预加载⭐⭐⭐⭐⭐⭐⭐⭐部分图片影响LCP
避免布局偏移⭐⭐⭐⭐⭐⭐⭐⭐⭐直接提升用户体验
缓存优化浏览器缓存⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐已经开启
Service Worker预缓存关键资源⭐⭐⭐⭐⭐
微前端优化共享依赖(React/Antd单例)⭐⭐⭐⭐⭐⭐⭐⭐⭐避免重复加载框架代码

难点二、踩坑

在已经达标后,我尝试做进一步性能优化时候,准备做工具库dayjs平替moment时遇到了小坑。

  • 问题1:package.json中去掉moment,但是发现打包后还是有moment包

    • 原因:antd中使用了moment的包,导致无法去除

    • 解决方案:

      • 升级antd为较新版本,新版本已支持dayjs
      • 使用官网推荐插件 antd-dayjs-webpack-plugin 解决
  • 问题2:项目中在处理 date 时使用的 API 为moment特有的,导致报错

    • 原因:moment中的部分api有两种命名,如:hours和hour,days和day,months和month等,但dayjs中仅有一种:hour,day,month

    • 解决方案:

      • 1.Day.js 官方提供了 pluralGetSet** 插件**,可以为所有时间单位添加复数形式的方法,实现与 Moment 的完全兼容
      • 2.暂时全局替换这部分使用错误的api,但需要注意排除可能潜在的风险。
    • 列表:最终选择了全局替换这部分使用错误的api,因为项目内使用数量不多,可以全方位进行回归,并且不许引入额外的插件。

可优化的点?

如果再迭代这个 TTI 性能优化项目,我会把之前的 「首屏一次性 TTI 优化」,全面升级为**「以用户真实体验为核心的 B 端中后台全生命周期性能管控体系」**,结合检测INP(Interaction to Next Paint,交互到下一次绘制时间)指标,**让性能优化从「一次性专项」变成「可持续的常态化管控」。

一、核心指标体系升级:从 TTI 平滑迁移到 INP

这是最核心的优化方向,也是解决当年项目「指标达标但体验差」的关键,依托TTI监控预留的 SDK 扩展能力,可低成本落地:

  1. 核心考核指标切换:把INP(第 98 百分位值) 作为业务核心性能考核指标,TTI 仅作为「首屏可用性」的辅助基线指标,保留历史数据做对比,彻底解决 TTI 覆盖不到用户后续交互的问题;
  2. 分场景精细化监控:针对 B 端用户的高频操作(表单输入 / 提交、表格筛选 / 翻页、SPA 路由切换、批量导出),做分场景 INP 监控,为每个核心场景设置单独的体验基线(如表单输入 INP≤150ms、路由切换 INP≤300ms),做到「精准监控,靶向优化」;
  3. 根因定位能力升级:利用 INP 的交互归因能力,在 SDK 中新增「交互元素、事件回调、代码栈」上报,让每一次 INP 偏高都能精准定位到具体的按钮 / 输入框、对应的点击 / 输入回调、甚至具体的代码行,优化效率提升 80% 以上。

二、技术层面深度优化:从「首屏优化」升级为「全流程交互体验优化」

之前优化主要集中在首屏 TTI,后续可以考虑针对 INP 的三个核心阶段(输入延迟、处理延迟、渲染延迟),做全生命周期的交互优化,覆盖用户从进入页面到离开的所有操作:

  1. 输入延迟优化:全流程长任务治理把当年的长任务拆分逻辑从首屏扩展到所有核心操作环节:将表格批量筛选、大表单数据校验、批量导出等 CPU 密集型逻辑全部移入 Web Worker,彻底脱离主线程;所有非紧急逻辑(埋点上报、状态同步、非核心数据预加载)均用requestIdleCallback执行,确保用户每一次操作都能被主线程快速响应,无排队延迟。

  2. 处理延迟优化:调研基于 React 18 并发特性做非紧急更新降级,考虑升级项目到 React 18,利用useDeferredValuestartTransition将大表单、大数据表格的非紧急渲染更新降级:比如表单输入时,优先更新输入框内容(让用户感受到「输入无延迟」),非核心的表单预览、实时校验延后执行;表格筛选时,优先更新筛选框,表格数据渲染标记为非紧急更新,避免重渲染阻塞用户操作。

  3. 渲染延迟优化:B 端高频场景专项优化

    • 大数据表格 / 列表全量切换为虚拟滚动,只渲染可视区域的 DOM 节点,减少 90% 以上的 DOM 数量,大幅降低重排重绘耗时;
    • 规范 CSS 动画和样式修改,用 transform/opacity 代替 top/left/width(前者仅触发重绘,后者触发重排),减少浏览器渲染耗时;
    • 核心交互组件(如按钮、输入框)做骨架屏 / 加载态优化,让用户操作后能快速看到视觉反馈,哪怕数据还在加载,也能感知到「操作已响应」。
  4. **资源加载优化:从「首屏优化」到「全链路资源治理」**当年仅做了首屏包体积优化,现在会做全链路资源治理:公共依赖抽离复用(将 React、AntD、echarts 等抽离为 CDN 外置依赖,避免每个路由单独打包);路由懒加载精细化(按业务模块拆分路由,做到「按需加载、按需编译」);静态资源极致优化(图片转 WebP/AVIF 格式、开启 CDN 多级缓存、小图标转 iconfont),进一步降低包体积和资源加载耗时。

三、工程化体系建设:从「一次性专项」到「可持续的性能管控闭环」

TTI优化是一次性的专项,没有做长期管控,可能会出现「业务迭代导致性能回退」的问题,后续可以搭建 「开发 - 构建 - 发布 - 运行」全流程性能管控体系 ,让性能优化成为团队的常态化工作,而非靠一个专项解决所有问题。定期查看线上性能指标数据,周会上进行分析,如果不达标及时跟进优化进度,分析性能回退的原因按月维度或者周维度进行治理。日常分享并沉淀性能优化的小技巧。将 INP/TTI 等核心性能指标纳入团队和业务迭代的考核标准,让团队所有人都重视性能,形成「人人关注性能、人人做性能优化」的氛围。

稳定性如何保证

我做了四层全流程保障,确保线上系统的稳定性,零故障落地优化,这也是大型生产系统做配置改造的核心:

  1. 开发阶段:保证SDK自身稳定,全链路try/catch包裹,哪怕某段逻辑报错,也不会影响 SDK 的核心功能,更不会向外抛出错误影响主业务;所有变量、参数都做空值兜底,比如event.error?.message || '',避免因属性访问报错导致 SDK 崩溃;保证SDK内部不修改原生原型例如Array/Object/Function等原生对象的原型,避免和主 JS 的代码冲突,导致 SDK 失效。
  2. 测试阶段:灰度环境多维度验证:将优化后的代码部署到测试环境,联合测试同学一起全量回归天机系统的核心业务模块(如DRP司机介绍人体系、外呼系统、审核中台),验证所有功能正常渲染、操作无报错,且通过本地Lighthouse复测,确认性能优化效果达标;
  3. 上线阶段:监控线上异常监控体系(我此前参与搭建的JS Error、白屏监测),确认无新增报错、白屏等问题;
  4. 上线后:72小时实时监控兜底:全量上线后,持续72小时监控线上的错误率、页面加载性能、接口响应速度等核心指标,同时保留回滚方案,周知团队内同学并及时同步进度和上线时间,保证与业务需求上线窗口期错开,若出现任何异常,可一键回滚到优化前的版本,彻底规避线上风险。

项目收益

最终的落地结果远超预期:优化后,天机系统整体包体积缩小40.6% ,页面TTI从7.2s降至4.8s,且全程零线上故障、零业务异常;同时,我把这次的优化配置、落地流程沉淀到团队工程化体系中,形成了前端性能优化的标准化模板,后续商家通等项目直接复用,既提升了性能,又节省了研发时间。

这次优化也让我深刻体会到,大型生产系统的技术优化,不是单纯的技术配置调整,而是“难点拆解-方案选型-风险控制-结果沉淀”的闭环,既要解决当下的问题,也要兼顾落地的稳定性和长期的工程化复用。

常见问题

问题一:分包策略是什么?

打包规则

• 首屏依赖:打到初始包,保证最快加载

• 低频大库:单独分包,异步加载

• 业务页面:路由懒加载,按需加载

• 缓存策略:不变的包(vendor)用长效缓存,业务包用内容哈希更新

通过这套分包,首屏包体积下降 40%+,首屏加载时间明显缩短,同时不影响线上稳定性。

分包:

基于以上打包规则,我当时的分包核心思路是:按“使用频率 + 业务稳定性 + 体积”三层拆分,做到首屏最小、公共复用、业务隔离。

  • 基础依赖包(vendor)

    ◦ React、ReactDOM、React-Router 等框架核心

    ◦ 单独打包,长期强缓存

  • 第三方工具包(utils)

    ◦ Lodash、Axios、类库、图表库等

    ◦ 体积大、更新少,抽成独立 chunk,避免业务包过大

  • 公共业务组件/公共逻辑(common)

    ◦ 多个页面都用到的业务组件、公共工具

    ◦ 避免重复打包,减少整体体积

  • 业务页面按路由分包

    ◦ 每个路由一个独立 chunk

    ◦ 只在进入页面时加载,首屏只加载必要代码

问题二:路由懒加载怎么做?

我项目里是基于 Umi + React 实现的路由懒加载,核心就三步:

  • 使用 React.lazy + import() 异步引入组件

    把原来的同步 import,改成动态 import,让组件变成异步 chunk,不会打包到主入口里。

  • 用 Suspense 做加载占位

    • 外层包 Suspense,指定 fallback(比如 loading 图标),避免页面空白。
  • 配合路由配置做按需加载

    在路由配置里,每个页面路由都用 lazy 包裹,

    只有当用户跳转到这个路由时,才去加载对应的 JS。

作用:

• 作用:拆分包、首屏只加载必要代码

• 效果:首屏体积变小、加载变快

• 注意:需要配合 webpack 分包 和 错误边界 防止加载失败白屏

import { lazy, Suspense } from "react";

import { Route } from "umi";

// 异步加载页面组件
const DriverList = lazy(() => import("@/pages/driver/list"));

function Routes() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <Route path="/driver/list" component={DriverList} />
    </Suspense>
  );
}

问题三、你是如何定位性能瓶颈的?

我定位 TTI 性能瓶颈的核心逻辑是:先统一指标口径解决「测不准」,再全链路拆分耗时定「宏观方向」,最后用工具做「微观根因定位」,结合线上真实用户数据补充验证,形成闭环,避免无效优化,确保每一步优化都有数据支撑,具体分为 以下 步骤,适配 B 端中后台 Umi+React 架构:

第一步:统一指标口径,解决「测不准」的前提问题

定位性能瓶颈的核心是「数据准确」,如果不同人、不同环境测出来的 TTI 结果不一致,后续定位全是无用功。所以我做的第一件事,就是统一全团队的性能测试和统计口径

  1. 计算规则统一:开发自定义的TTI性能测试SDK,完全对齐 Googleweb-vitals库的标准 TTI 计算逻辑,禁止任何自定义修改,保证和行业标准、Lighthouse/Chrome DevTools 口径一致;

  2. 线上统计规则统一:明确「冷启动首屏 TTI」为唯一统计对象(排除 SPA 路由切换的暖启动),新增无效数据过滤规则,统一用 TP90 分位值作为核心统计口径,同时新增 3 层无效数据过滤,排除异常样本对数据的干扰。

    1. 第一层:过滤掉因采集逻辑 bug、浏览器 API 异常、极端网络 / 设备导致的「明显不符合常理」的 TTI 数值,避免离谱数据拉偏整体统计结果。eg.过滤极小值:TTI<100ms(物理上不可能,大概率是采集逻辑bug)过滤极大值:TTI>30000ms(30秒,极端异常场景,无参考价值)过滤非数值/NaN:采集过程中API异常导致的无效值
    2. 第二层:过滤掉用户「未真正体验首屏」的场景,比如打开页面后秒关、后台打开标签页未激活,这类场景的 TTI 数据无法代表用户的真实感知。
// 第二层:用户行为异常过滤
function filterInvalidUserBehavior(metric) {
  // 1. 过滤短停留场景:页面加载后用户停留<3秒就关闭
  // 计算页面从导航开始到当前的停留时间
  const pageStayTime = Date.now() - performance.timing.navigationStart;
  if (pageStayTime < 3000) return false;
  
  // 2. 过滤后台加载场景:用户打开标签页但未激活(visibilityState=hidden)
  // 后台加载的页面,用户无任何视觉体验,TTI数值无意义
  if (document.visibilityState === 'hidden' && metric.startTime < 1000) return false;
  
  // 3. 过滤用户主动刷新场景:用户在首屏加载中刷新页面(非首次冷启动)
  const navEntry = performance.getEntriesByType('navigation')[0];
  if (navEntry?.type === 'reload') return false;
  
  return true;
}
  c.第三层:「场景合理性过滤」—— 过滤掉「非冷启动首屏」的场景(如 SPA 路由切换、子应用嵌套加载),确保数据仅对应「用户首次打开应用的冷启动首屏」,贴合我们的核心统计口径。
// 第三层:场景合理性过滤
function filterInvalidScene(metric) {
  // 1. 过滤SPA暖启动:路由切换导致的非冷启动场景(核心规则)
  if (!window.isPageColdStart) return false; // 该标记来自之前的路由埋点
  
  // 2. 过滤嵌套iframe场景:主应用嵌套子应用的iframe加载(非根页面冷启动)
  if (window.self !== window.top) return false;
  
  // 3. 过滤预渲染场景:浏览器预渲染页面(用户未真正访问,仅后台预加载)
  const navEntry = performance.getEntriesByType('navigation')[0];
  if (navEntry?.type === 'prerender') return false;
  
  return true;
}
第二步:全链路拆分耗时,定「宏观瓶颈方向」,避免局部瞎优化

B 端中后台的 TTI 耗时,是「资源加载 + 主线程执行 + 业务逻辑」共同决定的,我把 TTI 的完整生命周期拆分为3 个核心阶段,通过埋点统计每个阶段的耗时占比,先定位瓶颈在哪个大环节,再深入细节,避免「主线程问题没解决,先去优化图片大小」的低性价比操作:

  1. 资源加载阶段:从页面开始加载→FCP(首次内容绘制)完成,核心看首屏 JS/CSS 包体积、资源加载顺序、缓存策略;
  2. 主线程阻塞阶段:从 FCP 完成→TTI 达标,核心看 TBT(总阻塞时间)、长任务数量及来源;
  3. 业务逻辑执行阶段:首屏同步执行的业务逻辑(如权限校验、全局状态初始化、核心数据请求),核心看逻辑执行效率、接口响应速度。

当时通过拆分,我快速定位到项目的核心瓶颈:7**0% 的耗时在资源加载阶段,**20% 在主线程阻塞阶段,10% 在业务逻辑执行阶段,直接确定了「先优化资源加载,再治理主线程,最后微调业务逻辑」的优化优先级。

第三步:针对瓶颈阶段,用工具做「微观根因定位」,精准找到问题代码

针对拆分出来的核心瓶颈环节,我用专属工具做精准定位,而非全量扫一遍,大幅提升定位效率:

  1. 资源加载瓶颈定位:用webpack-bundle-analyzer分析打包产物,快速定位到大体积第三方库全量打入主包(echarts、AntD 未做按需引入)、非首屏路由未做懒加载静态资源未做压缩和 CDN 强缓存三大核心问题;同时用 Chrome DevTools 的「Network 面板」分析资源加载顺序,发现首屏加载了大量非核心资源,导致关键资源加载被阻塞。
  2. 主线程阻塞瓶颈定位:用 Chrome DevTools 的「Performance 面板」,录制首屏完整加载过程,通过「Main 主线程视图」精准找到所有超过 50ms 的长任务,定位到长任务来自首屏同步执行的全局状态初始化、埋点上报、复杂表单渲染;同时通过「Performance」的「Long Tasks」标签,统计长任务的总阻塞时间(TBT),明确优化重点。
  3. 业务逻辑瓶颈定位:用「Network 面板」分析首屏接口,发现部分非核心接口同步请求导致首屏渲染被阻塞;同时通过业务代码审计,发现部分权限校验逻辑存在重复计算,增加了主线程执行耗时。
第四步:线上真实用户数据补充,覆盖实验室看不到的问题

实验室只能覆盖标准化环境,而线上用户的设备、网络、浏览器千差万别,所以我在采集 SDK 中新增用户环境信息上报(设备 CPU 核心数、浏览器版本、网络类型),把线上 TTI 数据按这些维度做拆分,最终发现:低性能办公本、内网弱网环境的用户 TTI 超标最严重

针对这一问题,我做了针对性降级优化:优先加载页面骨架屏对弱网环境用户进行友好优化,联系地方运维/IT提出优化需求,尽量让优化效果覆盖到所有用户。

思考沉淀(升维,体现方法论)

定位性能瓶颈的核心,从来不是上来就用工具瞎找,而是**「先拆链路,先定方向,再抠细节」**。尤其是 B 端中后台应用,业务逻辑复杂、依赖包多,必须先跳出具体的业务代码,从全链路视角找到核心瓶颈,再用工具精准定位问题代码,不然永远是治标不治本,做很多无意义的优化。

问题四、为什么你们选择TTI作为优化指标?

因为天机主要面向的是公司内部的运营、客服、业务同学。他们主要的业务场景是,搜索查看以及操作司机数据。主要诉求是与页面进行操作,而不是浏览页面。所以他们更关注 “看得见、摸不着” 的状态。所以选了TTI页面可交互市长这个指标进行了优化。

为什么没选INP?

  • 我做优化的时候是24年启动的,当时组内是比较看中先优化TTI的,并且INP这个概念也没有广泛流传。TTI 是行业比较火热的合规选择。

  • 当时的业务同学也是比较关注**「页面什么时候达到完全可交互状态」**,符合TTI的核心定义。而 INP 衡量的是页面全生命周期的交互延迟,解决的是「用的爽不爽」的进阶体验问题,是首屏可用性问题解决之后,才需要做的优化迭代,当时走的是「先保可用,再提体验」的项目迭代逻辑。

  • 还有当时业内对TTI的优化方法论已经非常成熟,从包体积拆分、资源加载优化、主线程长任务拆解,都有明确的落地路径,我在 3 个月内就完成了全链路优化,拿到了「TTI 从 7.2s 降至 4.8s、包体积缩小 40.6%、用户操作卡顿率下降 28%」的可量化结果,完全匹配 OKR 要求;而当时 INP 还处于实验阶段,没有成熟的优化方法论、没有配套的根因定位工具、没有行业通用的基线标准,甚至 Chrome DevTools 都没有专门的 INP 分析面板,如果当时强行切换到 INP,不仅无法快速拿到结果,还会导致整个性能体系没有统一的衡量基线,无法验证优化效果。

  • 前瞻性布局:不是没考虑 INP,而是做了全链路的兼容设计,为后续平滑升级预留了空间

  • 我在做方案设计的时候,也关注到了 Google 提出的 INP 实验性指标,所以从一开始就做了兼容布局的口子,没有做 “一次性优化”:

    • 底层采集 SDK 完全基于 Google 官方web-vitals库开发,预留了 INP 的采集扩展能力,不需要重构 SDK,只需要加 3 行代码就能完成 INP 的全量采集、上报、监控,改造成本极低;
    • TTI 优化的核心抓手,就是降低 TBT(总阻塞时间)、拆分主线程长任务,而长任务优化,恰恰也是 INP 优化的核心手段 —— 当时做的所有分包优化、长任务拆分、非核心逻辑异步降级,不仅降低了 TTI,也同步优化了页面全周期的交互延迟,为后续切换到 INP 打下了完整的基础,没有做任何无用功。

问题五、目前TTI已经lighthouse10移除了,你知道取而代之的是哪个性能指标吗?为什么会换成这个新的

取而代之的指标:
  • INP(Interaction to Next Paint,交互到下一次绘制时间)

    • 2024 年 3 月 Google 正式将其纳入 Web Vitals 三大核心指标(另外两个为 LCP、CLS),同时取代了 TTI 的「页面可交互性核心衡量指标」定位,INP 衡量页面整个生命周期内,用户所有交互的响应延迟情况,最终取所有交互延迟的第 98 百分位值(排除极端异常值)作为页面的 INP,核心是「用户每一次操作的响应速度,才是真正的可交互体验」。

      • 计算逻辑:

        • 监听范围:覆盖用户三类核心交互(点击、触摸、键盘输入),完全匹配 B 端用户的所有操作场景(按钮点击、表单输入、菜单切换、表格操作等);
        • 单交互延迟计算:单次交互延迟 = 从用户触发操作,到浏览器完成下一次画面绘制的总耗时,完整包含「输入事件处理时间、主线程长任务阻塞时间、渲染绘制时间」全链路;
        • 最终值确定:页面生命周期结束后,取所有交互延迟的第 98 百分位值(而非最大值,过滤极端异常值),作为该页面的最终 INP;
        • 官方评级标准:INP ≤ 200ms 为优秀,200ms <INP ≤ 500ms 为需要改进,INP> 500ms 为不合格。
  • 辅助配套指标:TBT(Total Blocking Time,总阻塞时间)

    • 解释:

      • Total Blocking Time(TBT)总阻塞时间,衡量在首次内容绘制 (FCP) 之后,主线程处于阻塞足够长的时间以防止输入响应所用的总时长。作为首屏阶段的补充指标,和 INP 形成「首屏阻塞兜底 + 全生命周期交互体验」的完整覆盖。
      • 上面那句话特别绕,其实我感觉就是在FCP之后,主线程被阻塞的总时间,如果时间很长,相当于FCP之后的很长一段时间内,页面都是不可交互的。
为什么TTI被淘汰了?

TTI 的设计缺陷是被替代的根本原因,尤其在你深耕的 B 端中后台场景,缺陷被无限放大,也是大厂逐步放弃 TTI 的核心原因:

TTI 的核心缺陷对真实体验的影响
只关注首屏一次性指标,完全覆盖不到用户核心操作TTI 仅衡量「页面首次达到完全可交互的时间」,但 B 端用户 80% 的操作(按钮点击、表单输入、路由切换、表格筛选)都发生在首屏加载完成后,TTI 完全无法捕捉这些核心场景的体验好坏,哪怕首屏 TTI 达标,用户点击按钮有 500ms 延迟,体验依然极差。
计算规则过于苛刻,极易失真,和用户感知脱节TTI 要求 FCP 后必须有连续 5s 无超过 50ms 长任务的静默窗口,才能判定达标。但 B 端中后台首屏后必然存在异步埋点、定时器、数据预加载等逻辑,很容易打破静默窗口,导致 TTI 被无限拉长 —— 出现「用户已经能正常操作表单,TTI 却仍未达标」的离谱情况,指标完全无法反映真实体验。
是模糊的结果性指标,优化指导性极差TTI 高了,你无法精准定位是资源加载、长任务、还是后续异步逻辑的问题,根因定位难度极大;
无法衡量用户最在意的「交互响应速度」用户对「页面能不能用、卡不卡」的最直观感知,是「我点了按钮,多久能看到反馈」,而 TTI 只看「页面什么时候能交互」,没有关注交互发生后的响应延迟,核心体验盲区较大大。
为什么 INP 能完全替代 TTI?
  • 覆盖全生命周期,完全匹配 B 端用户的核心操作场景INP 关注用户从进入页面到离开的所有交互动作,而不是只看首屏一次性加载。对于你的天机 iframe+Umi SPA 架构,子应用内部的路由切换、司机审核表单操作、外呼系统按钮点击、商户列表筛选等所有业务操作,INP 都能精准捕捉,完全覆盖 B 端用户 80% 的核心使用场景,而 TTI 只能测子应用首屏加载的那一次,完全没有业务价值。
  • 指标与用户真实体验 100% 对齐,无失真INP 直接衡量「用户操作后多久能看到视觉反馈」,这是用户对「页面卡不卡」最直观的感受,没有 TTI 那种苛刻的静默窗口规则 —— 哪怕页面后台有异步埋点、定时器在跑,只要不影响用户的交互响应,INP 就不会受影响,绝不会出现「用户能用但指标不达标」的情况。
  • 优化指导性极强,根因定位零成本INP 的每一个异常值,都对应着用户的一次具体交互,能精准定位到「哪个按钮点击、哪个输入操作导致了延迟」,进而追溯到对应的代码逻辑、长任务、接口请求,优化目标极其明确;而 TTI 是模糊的结果性指标,很难定位根因,这也是大厂性能优化全面转向 INP 的核心原因。
  • 适配现代前端架构,无场景盲区不管是 SPA 单页应用、iframe 多应用架构、微前端、服务端渲染,INP 都能完美适配,只要有用户交互就能捕捉;而 TTI 对 SPA 路由切换、iframe 子应用切换等场景完全无能为力,只能靠自定义规则补充,指标口径无法统一。
  • 官方全链路支持,行业标准统一目前 Chrome 开发者工具、Lighthouse、PageSpeed Insights 都已把 INP 作为核心可交互指标,web-vitals 库原生支持 INP 采集,和你之前开发的 TTI SDK 完全兼容,改造成本极低;同时阿里、字节、美团等大厂都已把 INP 作为前端性能的核心考核指标,行业标准完全统一。

问题六、为什么自己测算跟实际有偏差?

只要用 Google 官方**web-vitals**库,且 SDK 内部的二次过滤规则合理,偏差会控制在 ±5% 以内,属于可接受范围;如果偏差过大,主要是以下 3 个原因:

偏差原因 1:测试环境不一致(最常见)

Lighthouse/Chrome DevTools 的网页版测试,默认是固定的标准化环境

  • 浏览器:Chrome 稳定版,无痕模式,禁用所有插件;
  • 网络环境:Fast 3G 节流(下载 1.6Mbps、上传 750Kbps、RTT 150ms);
  • CPU 环境:4x CPU 节流(模拟中低端设备);
  • 测试前提:冷启动,清除浏览器缓存,前台聚焦。

而 SDK 采集的是真实用户的千差万别的环境

  • 设备:从高端 Mac 到低端办公本,CPU / 内存差异极大;
  • 网络:从 5G 到 2G,从内网到外网,RTT / 带宽差异极大;
  • 场景:页面可能后台挂起、用户中途切换标签页、浏览器可能有插件干扰。

解决方法

  • 验证时,严格对齐 Lighthouse 的标准化环境;
  • SDK 内部做严格的无效数据过滤(如页面后台挂起、加载超过 60s 的极端值);
  • 线上核心看TTI 95 分位值,而非平均值 —— 平均值会被高性能设备用户拉高,95 分位值才能代表绝大多数普通用户的真实体验。
偏差原因 2:SDK 内部的二次过滤规则不合理

如果 SDK 内部的二次过滤规则(如采样率、无效数据判定)和 Lighthouse 的默认规则不一致,也会导致偏差:

  • 比如采样率过低,可能会漏掉一些极端值或典型值;
  • 比如无效数据判定过严,可能会过滤掉一些正常的 TTI 数据;
  • 比如无效数据判定过松,可能会混入一些异常的 TTI 数据。

解决方法

  • SDK 内部的二次过滤规则,尽量和 Lighthouse 的默认规则对齐,及时调整过滤规则;
  • 调整过滤规则后,先在测试验证,确认偏差在可接受范围内;
偏差原因 3:SPA 路由切换的自定义 TTI(仅针对 B 端 SPA 场景)

标准 TTI 是 W3C 定义的针对页面首次导航加载的指标,浏览器 Performance API 只会在页面首次加载时生成 navigation 条目,SPA 路由切换不会触发新的 navigation,所以 web-vitals 的 onTTI 只会在首屏触发一次。

如果 SDK 内部为了覆盖 SPA 路由切换场景,自定义了路由 TTI 的计算规则(比如把静默窗口从 5s 改成 500ms,更贴合 B 端用户对操作响应的高要求),那这个自定义的路由 TTI,和 Lighthouse 的标准首屏 TTI,本身就不是同一个指标,没有可比性,偏差自然会很大。

解决方法

  • 明确区分「首屏标准 TTI」和「路由切换自定义 TTI」,分别采集、分别统计、分别分析;
  • 首屏标准 TTI,严格用 web-vitals 的 onTTI 回调,和 Lighthouse 对齐;
  • 路由切换自定义 TTI,单独定义业务规则,单独设置告警阈值。