前端性能优化的第一性原理,是不断缩短“用户发起意图 → 获得可用结果”之间的时间

文章来源声明: 原文作者:CharlesYu01; 来源站点:掘金; 原文链接:https://juejin.cn/post/7684180482599682102; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

该方案抓住感知性能这一杠杆,用Native骨架屏和Ready通知把等待藏起来,成本可控、落地清晰,适合Hybrid H5、营销转化页及App内嵌页优化。

综述 --

前端性能优化的本质,不只是让代码跑得更快,而是重新安排工作发生的时间:能前置的前置,能隐藏的隐藏,最后再优化无法绕开的关键路径。

层级高度抽象结论核心问题典型策略
**L1 目标层****缩短“用户意图 → 可用结果”的时间**用户到底等了多久?以用户等待时间而不是单一技术耗时作为最终目标
**L2 原则层****减少真实等待 + 减少感知等待**等待能不能消灭?不能消灭时能不能隐藏?一部分真正提速,一部分改善等待体验
**L3 策略层 A****把工作前置**哪些事情没必要等用户点击后再做?预加载、预创建、预连接、预取、预计算
**L3 策略层 B****把等待体验前置到 App**无法避免的等待,由谁来承接?Native 容器、骨架屏、Loading、页面框架、转场
**L3 策略层 C****压缩剩余关键路径**真正必须发生在点击后的工作还能快多少?网络、JS、接口、渲染、缓存、并行化
**L4 工程层****技术方案只是上层原则的实现**用什么手段落地?WebView Pool、预热、DNS 预解析、Skeleton、JSBridge、Bundle 拆分等

Hybrid H5 首屏体验优化:把骨架屏前置到 Native,解决白屏和页面跳变

在 App 里打开 H5 页面,是一个非常常见的 Hybrid 场景。

但很多 Hybrid 页面都会遇到一个类似的问题:

用户点击入口之后,需要等待 1~2 秒,才能真正看到页面内容。

这 1~2 秒可能表现为:

  • 白屏
  • Loading
  • 页面框架不断变化
  • TitleBar 先出现又消失
  • 页面加载完成后突然跳一下

从技术指标来看,1~2 秒似乎不算特别夸张。

但从用户体验来看,这段时间非常危险。

因为用户不知道:

页面到底是在加载,还是已经卡住了?

尤其是在营销、金融、电商等强转化场景下,用户很可能还没有真正看到页面,就已经关闭了页面。

最近在思考 Hybrid 页面的性能优化时,我发现一个很重要的方向:

不一定要先把 H5 从 2 秒优化到 1 秒,而是应该先想办法,让用户在几十毫秒内看到一个“已经响应”的页面。

于是有了下面这套方案。


一、现有 Hybrid 页面的问题

先看一个比较典型的页面打开过程。

用户点击入口
    ↓
Native 创建 WebView
    ↓
WebView 打开 <span>H5</span>
    ↓
<span>H5</span> JavaScript 初始化
    ↓
判断页面是不是全面屏
    ↓
决定是否展示 TitleBar
    ↓
展示 Loading
    ↓
请求接口
    ↓
渲染页面
    ↓
用户看到真正内容

这里面存在两个明显的问题。

1. 用户第一眼看到的是“加载状态”

例如:

点击
 ↓
白屏
 ↓
Loading
 ↓
页面

或者:

点击
 ↓
一个空页面
 ↓
Loading
 ↓
页面

虽然系统其实已经开始工作了,但用户得到的反馈非常弱。

这也是很多 H5 页面关闭率比较高的重要原因之一。


2. 页面形态确定得太晚

有些 Hybrid 页面还存在另一个问题。

页面到底:

  • 是否全面屏
  • 是否展示 Native TitleBar
  • 状态栏是什么颜色
  • 页面背景色是什么
  • 顶部间距是多少

这些信息,本来在打开页面之前就可以知道。

但很多系统仍然会等 H5 加载后,再由 H5 判断。

例如:

先展示 TitleBar
      ↓
<span>H5</span> 初始化
      ↓
读取参数
      ↓
发现是全面屏
      ↓
隐藏 TitleBar

用户就会看到页面突然变化一下。

这种问题虽然只有几百毫秒,但非常影响页面的精致感。


二、核心思路:能前置的事情,不要等 H5 启动后再做

这次优化的核心原则其实很简单:

如果一个信息在打开 H5 之前就已经能够确定,就不要等 H5 JavaScript 运行之后再确定。

于是整个链路可以改成:

用户点击
    ↓
Native 读取页面配置
    ↓
立即确定页面形态
    ↓
Native 展示骨架屏
    ↓
后台加载 <span>H5</span>
    ↓
<span>H5</span> 首屏准备完成
    ↓
通知 Native
    ↓
关闭骨架屏
    ↓
用户看到真实页面

这样,用户看到的过程就变成了:

点击
 ↓
骨架屏
 ↓
真实内容

而不是:

点击
 ↓
白屏
 ↓
Loading
 ↓
页面变化
 ↓
真实内容

这就是整个优化的核心。


三、优化一:把页面容器形态前置到 Native

第一个优化并不是骨架屏。

而是:

页面框架应该尽量在 WebView 创建之前确定。

例如,可以在 URL 或页面路由配置中增加一些信息。

<span>pageStyle</span>=fullscreen
<span>titleBar</span>=<span>false</span>
<span>statusBarStyle</span>=dark
backgroundColor=<span>#FFFFFF</span>

Native 在收到跳转请求的时候,就能够直接知道:

这是一个全面屏页面。

于是 Native 可以直接:

创建全面屏容器
 ↓
设置状态栏
 ↓
设置背景色
 ↓
创建 WebView

而不是:

创建普通页面
 ↓
加载 <span>H5</span>
 ↓
<span>H5</span> 判断
 ↓
通知 Native
 ↓
修改页面样式

这个变化看起来很小,但会解决大量页面闪动问题。

它本质上是在做一件事:

把运行时决策,提前变成页面打开时决策。


四、优化二:把骨架屏前置到 Native

接下来才是整个方案最核心的一步:

在 H5 加载之前,由 Native 直接展示骨架屏。

比如页面配置中增加:

<span>skeletonType</span>=product

或者:

<span>skeletonType</span>=form

Native 根据不同类型展示一个对应的 Skeleton。

例如:

┌────────────────────┐
│                    │
│ ███████████████    │
│ █████████          │
│                    │
│ ┌────────────────┐ │
│ │                │ │
│ │                │ │
│ └────────────────┘ │
│                    │
│ █████████████      │
│ █████████          │
│                    │
└────────────────────┘

这里有一个很重要的原则:

Native 骨架屏没有必要和真实 H5 页面做到 1:1。

否则成本会非常高。

骨架屏只需要做到三件事情:

  1. 页面整体结构类似;
  2. 页面尺寸基本一致;
  3. 用户切换到真实页面时,不产生明显跳变。

也就是说:

它的目标不是复制页面,而是让用户获得稳定的加载预期。


五、为什么 Native 骨架屏比 H5 骨架屏更有价值?

可能有人会问:

H5 自己做 Skeleton 不就可以了吗?

当然可以。

但这里存在一个关键问题。

H5 Skeleton 自己也需要等待:

WebView 初始化
 ↓
<span>HTML</span> 加载
 ↓
JS 下载
 ↓
JS 执行
 ↓
框架初始化
 ↓
Skeleton 渲染

也就是说:

H5 骨架屏本身,也属于 H5 加载链路的一部分。

如果真正的问题是:

WebView 打开后的前 500ms~1000ms 什么都看不到

那么 H5 Skeleton 很难完全解决。

而 Native Skeleton 可以更早出现:

用户点击
 ↓
Native 页面创建
 ↓
立即显示 Skeleton

理论上几十毫秒就可以出现。

两者解决的问题并不完全一样。

可以简单理解为:

Native Skeleton
解决「<span>H5</span> 出现之前」的问题

<span>H5</span> Skeleton
解决「<span>H5</span> 已经运行,但数据还没回来」的问题

两者甚至可以组合使用。


六、优化三:不要用 onPageFinished 判断页面是否可用

Skeleton 展示出来之后,还有一个非常关键的问题:

到底什么时候关闭 Skeleton?

一个很容易想到的方案是使用:

onPageFinished

但实际上并不推荐这么做。

因为:

WebView 加载完成,不等于用户可以使用页面。

例如:

<span>HTML</span> 加载完成
 ↓
onPageFinished
 ↓
但 JS 还在初始化
 ↓
接口还没回来
 ↓
页面还没有真正渲染

如果这时候关闭 Skeleton,用户仍然可能看到:

Skeleton
 ↓
白屏
 ↓
页面

反而失去了意义。


七、让 H5 主动告诉 Native:我准备好了

更合理的方案是:

谁最了解页面什么时候可以展示,就让谁来发出 Ready 信号。

也就是 H5 主动通过 JSBridge 通知 Native。

例如:

<span>window</span>.<span>NativeBridge</span>.<span>pageReady</span>()

或者更明确一些:

window<span>.NativeBridge</span><span>.pageReady</span>({
  phase: <span>'firstScreen'</span>
})

整体流程变成:

Native 展示 Skeleton
        ↓
<span>H5</span> 后台加载
        ↓
接口返回
        ↓
首屏完成渲染
        ↓
<span>H5</span> 调用 pageReady
        ↓
Native 隐藏 Skeleton

这时候 Skeleton 的生命周期,才真正和用户看到的页面状态对应起来。


八、Skeleton 不要直接消失,可以做一个短暂淡出

还有一个体验细节。

Native Skeleton 和真实 H5 页面不可能完全一样。

如果直接:

Skeleton → 页面

可能会感觉突然闪了一下。

可以增加一个很短的过渡动画:

<span>100</span>ms~<span>200</span>ms Fade <span>Out</span>

即:

Skeleton
   ↓
Skeleton <span>opacity</span> <span>1</span> → <span>0</span>
   ↓
<span>H5</span> 页面显示

这样可以很好地掩盖 Native Skeleton 与真实页面之间的小差异。


九、最终页面打开链路

优化前:

用户点击
    ↓
打开 WebView
    ↓
白屏
    ↓
<span>H5</span> 初始化
    ↓
判断页面样式
    ↓
调整 TitleBar
    ↓
Loading
    ↓
接口返回
    ↓
页面渲染

优化后:

用户点击
    ↓
Native 读取页面配置
    ↓
确定页面容器形态
    ↓
立即展示 Skeleton
    ↓
WebView 后台加载
    ↓
<span>H5</span> 初始化
    ↓
接口返回
    ↓
首屏 Render
    ↓
<span>H5</span> → pageReady
    ↓
Native Skeleton Fade Out
    ↓
真实页面

对于用户来说,感受到的是:

点击
 ↓
页面结构立即出现
 ↓
内容加载完成

整个过程会连贯很多。


十、这个方案并没有真正让 H5 快 1 秒

这里有一个很重要的认知。

假设优化之前:

<span>H5</span> 首屏加载时间:<span>1500ms</span>

优化之后:

<span>H5</span> 首屏加载时间:仍然是 <span>1500ms</span>

从技术性能来看:

可能一点都没变。

但用户体验已经从:

1500ms 白屏

变成:

50ms 出现 Skeleton
1450ms 后出现真实内容

这属于典型的:

感知性能优化。

Performance Optimization 不应该只关注:

页面到底快了多少毫秒?

还应该关注:

用户觉得它快不快?

这两个问题并不是完全一样的。


十一、真正应该观察的指标,也需要变化

做完这个优化以后,如果仍然只观察:

页面加载时间

可能看不到很明显的收益。

更应该关注这些指标。

第一类:技术指标

例如:

WebView 创建耗时

Skeleton 首次展示时间

<span>H5</span> Ready 时间

Skeleton 持续时间

FCP

LCP

TTI


第二类:用户行为指标

这部分可能更加重要。

例如统计:

0~500ms 页面关闭率

500ms~1s 页面关闭率

1s~2s 页面关闭率

页面首屏到达率

页面停留时长

后续点击率

最终转化率

因为这套方案真正希望解决的是:

用户还没有看到页面,就已经离开。

所以「早期关闭率」会是一个非常值得观察的指标。


十二、先优化“感知性能”,再优化“真实性能”

整个优化最好拆成两个阶段。

第一阶段:解决用户第一眼体验

先完成:

容器形态前置
<span>+</span>
Native Skeleton
<span>+</span>
H5 Ready Signal

目标很简单:

点击页面之后,不再出现明显白屏。


第二阶段:继续优化 H5 真正的加载速度

然后再继续分析:

WebView 创建
 ↓
<span>HTML</span> 加载
 ↓
JS 下载
 ↓
JS Parse
 ↓
JS Execute
 ↓
框架初始化
 ↓
接口请求
 ↓
页面 Render
 ↓
用户可交互

每个环节都可以继续优化。

例如:

  • WebView 预创建
  • WebView Pool
  • DNS 预解析
  • HTTP 预连接
  • H5 资源预加载
  • JS Bundle 拆分
  • 静态资源缓存
  • 首屏代码瘦身
  • 非首屏组件懒加载
  • 接口并行
  • 首屏数据预取
  • SSR
  • Native 数据提前请求

这时候才能真正让:

1500ms

进一步变成:

1000ms
800ms
甚至更低


十三、更进一步:Hybrid 页面其实可以越来越“前置”

做到这里之后,会发现一个非常有意思的规律:

很多原本属于 H5 初始化阶段做的事情,其实都可以往前移动。

最开始只是:

页面是不是全面屏

后来可能是:

TitleBar 样式

然后是:

Skeleton 类型

再进一步可能是:

登录态

用户基础信息

实验参数

页面配置

甚至首屏接口数据

于是整个 Hybrid 页面的优化方向会逐渐变成:

传统模式:

进入 <span>H5</span>
 ↓
什么都不知道
 ↓
重新获取所有信息
 ↓
开始渲染

逐渐变成:

进入 <span>H5</span> 之前
 ↓
Native 已经准备好了大量上下文
 ↓
<span>H5</span> 直接消费
 ↓
快速渲染

这也是 Hybrid 页面进一步做到“秒开”的重要方向。


十四、总结

这次优化表面上看,是:

给 Hybrid H5 加一个 Native 骨架屏。

但我认为真正值得沉淀的是背后的设计原则:

能在页面打开之前确定的信息,就不要等 H5 运行之后再确定。

整个方案可以总结为三个动作:

<span>1.</span> 容器形态前置

<span>2.</span> Skeleton 前置到 Native

<span>3.</span> H5 主动发出 Page Ready 信号

最终让用户体验从:

点击
 ↓
等待
 ↓
白屏
 ↓
Loading
 ↓
页面

变成:

点击
 ↓
页面框架立即出现
 ↓
真实内容自然填充

它可能并没有第一时间让 H5 的真实加载时间减少很多。

但它解决了另一个同样重要的问题:

让用户感觉页面已经立刻响应了。

而在很多业务场景中,性能优化最终关注的并不是:

少了多少毫秒。

而是:

因为这几百毫秒的体验变化,少流失了多少用户。

如果再继续往后演进:

Native Skeleton
 ↓
WebView 预热
 ↓
资源预加载
 ↓
接口预取
 ↓
Native / <span>H5</span> 数据共享

最终就不再只是一次“骨架屏优化”,而会逐渐形成一套完整的:

Hybrid 页面秒开体系。

性能优化十句大白话

少让用户等,提前把能做的做掉,剩下的等待想办法藏起来。

  1. 能消灭的等待提前消灭,不能消灭的等待尽量让用户感知不到。
  2. 能在用户点击之前做的事,就别等用户点击以后再做。
  3. 用户不关心你加载了多少资源,只关心什么时候能看到、什么时候能用。
  4. 页面不一定真的要快很多,但一定要让用户感觉它马上有反应。
  5. 与其打开页面后再准备,不如在用户还没进来时先准备好。
  6. H5 还没准备好时,先让 App 把场子撑起来,别把白屏留给用户。
  7. 能并行做的就别排队,能后台做的就别挡在用户前面。
  8. 不是所有代码都值得优化,优先优化真正卡住用户的那一段。
  9. Loading 不是性能优化的终点,它只是让等待没那么难受。
  10. 性能优化的本质,就是想办法少让用户等、晚让用户等、甚至让用户感觉不到自己在等。