首屏直出(IFR) v0.5
首屏直出(Instant First-Frame Rendering,IFR)会在 loadTemplate 期间、后台线程启动之前绘制真实内容。Vue Lynx 将 IFR 与元素模板(Element Templates,ET)组合使用:IFR 消除等待后台线程造成的白屏,ET 则降低主线程同步渲染静态结构子树的成本。
配置
在 lynx.config.ts 中开启 IFR:
开启 IFR 时会自动开启元素模板。典型组件树不需要修改应用代码。
如果要在保留 IFR 的同时二分元素模板问题,请显式关闭 ET:
也可以通过 enableElementTemplates: true 在普通后台渲染中独立开启 ET。这是高级组合方式,不是推荐的 IFR 配置。
IFR 为什么能消除白屏
没有 IFR 时,可见内容必须等待后台线程启动、执行应用、完成 Vue 首次渲染,再把 ops 批次发回主线程:
开启 IFR 后,主线程 bundle 会携带 Vue 运行时和应用代码。Vue 在 loadTemplate 内同步渲染,同时后台线程并行启动:
因此首屏可以在任何后台 JavaScript 运行之前出现。Hydration 完成后,树由后台线程接管,后续更新全部走正常 ops 管线。
Hydration
两个线程使用相同初始数据执行同一个应用。主线程会记录自己应用的每一批 ops;后台线程的初始批次不直接重复执行,而是与记录逐批协调:
- 完全一致的批次会被跳过,因为内容已经在屏幕上。
- 文本、样式和属性差异会原地 patch。
- 结构差异会拆除首屏树,再按后台 ops 重建。
正确性不依赖两次渲染一致。结构不一致只会失去该屏的 IFR 性能收益,并在开发环境输出 [vue-lynx] IFR hydration mismatch 警告。
事件 sign 和元素 ID 在两次渲染中保持确定性。后台 handler registry 就绪后,首帧元素的事件会直接路由到正常的 Vue handler,无需重新绑定。
元素模板(Element Templates)
IFR 决定首帧何时渲染;元素模板减少这次渲染需要多少工作。
普通路径中,每个静态元素仍要创建 vnode 和 shadow node、产生多条 ops、跨线程传输,再由主线程解释器分发。编译器可以把符合条件的子树折叠为:
静态骨架会变成编译器生成的 JavaScript create() 函数。Vue 为整棵子树只发送一条 INSTANTIATE_TEMPLATE op;动态文本、class、样式和属性作为“洞”,继续通过普通 SET_* ops 更新。
哪些结构会下沉
当子树结构在编译期完全可知时,它可以下沉:每个节点都是普通 Lynx 元素,只有属性值或文本内容是动态的。
以下结构保留在普通 vnode 路径:
- 组件、slot、
v-if/v-for宿主;它们内部的普通元素结构仍可下沉 - 内部节点上的运行时指令、ref、key、ID 和 vnode hook
- 有专用主线程行为的
<list>和<list-item> - 注释和混合动态文本段
模板根节点会保留正常 vnode props 和指令。Scoped CSS 受到完整支持:编译器会把组件 CSS scope ID 烘焙到每个下沉元素。只有确认与普通样式路径语义一致时,静态 inline style 才会被烘焙。
框架模板,不是引擎二进制模板
Vue Lynx 当前不使用 Lynx 的二进制 elementTemplates bundle section、__ElementFromBinary 或 __GetTemplateParts。这里的元素模板是框架层优化,由普通 typed Element PAPI 调用组成。
它消除的是 vnode、逐节点 ops、序列化和解释器分发,同时保留现有 renderer 与 hydration 协议。未来可以让二进制模板后端复用相同的静态骨架和洞元数据,但它不属于当前实现。
语义保证
下沉是优化,不是新的渲染模式。不符合条件的结构会自动回退;下沉和未下沉的树必须产生相同的最终文档与更新结果。内部匿名节点不会携带 Vue 的 selector bookkeeping 属性,但任何带 ref、ID 或其他身份要求的元素都不会进入匿名内部路径。
编写 IFR 友好的首屏
初始渲染会在每个线程各执行一次,因此它应表现为输入的纯函数:
- 保持首屏输出确定性。渲染路径中避免
Math.random()、Date.now()和依赖线程的分支。 - 把数据请求、定时器、订阅和其他副作用放进组合式 API 生命周期钩子。这些钩子会在主线程渲染期间被抑制,只在后台线程运行。
- Options API 的
mounted()目前不会在主线程被抑制。使用 IFR 的首屏组件应优先使用组合式 API 钩子。 - IFR 只能绘制同步可用的数据。fetch 驱动页面仍应 mount,并在主线程画出有意义的外壳或骨架——用例如
useQuery({ enabled: !isIfrMainThread() })关掉网络请求,而不是整段跳过app.mount()。跳过 mount 只会留下更大的 IFR bundle,却拿不到首屏收益。 - 后台 handler registry 就绪前的短暂窗口中,普通事件可能被丢弃。主线程脚本 handler 本来就在主线程,因此仍可交互。
取舍
- IFR 会在两个线程 bundle 中都放入 Vue 运行时和应用。全示例中
main.lynx.bundlegzip 体积的中位增长约为 2.26 倍。 - 应用会在两个线程各求值一次。全示例串行工作量的 TTI proxy 增长约 35–36%;在真机上,主线程渲染会与后台启动重叠。
- IFR 无法加速只有异步请求完成后才存在的内容。
- CSS Modules 的 class 名可能在首帧尚未哈希,并在 hydration 时 patch 为最终名称。
对体积敏感或完全由 fetch 驱动的首屏应先实测。对于具有同步初始数据的内容型首屏,IFR + ET 是推荐配置。
Benchmark
下表把三种产品配置明确分开。FCP 来自 Lynx for Web 的真实 Web Worker,hello-world 无 CPU 节流测量;渲染成本来自约 1000 元素动态内容场景的 V8 --jitless warm 中位;TTI 和 gzip 是全示例扫描相对 No IFR 归一化后的中位数。这些列来自不同轮次,不应视为同一条端到端 trace。
跨多轮测量,内容优先首屏在 Lynx for Web 上的 IFR + ET FCP 收益大致落在 −12% 到 −26%(十个 demo 中位 −19%;后续含 TodoMVC / gallery / 更大应用的 七应用集中位约 −12%)。绝对毫秒随宿主漂移;比值才是稳定货币。详见 IFR 性能数据。
数据说明了什么
- IFR 在内容优先首屏上的收益是真实的、结构性的。有真实线程边界的 Lynx for Web 会把后台启动 + IPC 移出关键路径——这也是单进程 benchmark 看不到它的原因。一轮(十个偏小 example)看到 −8% 到 −23%,中位 −19%; 后续含 TodoMVC、gallery、Hacker News(shell IFR)、AI Chat、Elk 的复测 中,内容优先中位约 −12%(hello-world 单独约 −22% 到 −26%,视宿主而定)。 ReactLynx 在第一轮 harness 上为 −23%,同属这一量级。
- 元素模板攻击的正是 IFR 新增的那笔成本。主线程同步渲染跨复测约便宜 6–15×,ops 载荷缩小 3–1000×。小屏 web FCP 上这常常看不出来——两种 IFR 配置通常只差几个百分点——但渲染成本随屏幕变大、CPU 变慢而增长, 因此 ET 默认随 IFR 开启。
- 代价是 bundle 体积,大应用 / fetch 重应用可能反转。
main.lynx.bundlegzip 约 ×2.2–2.5(套件中位 ~×2.26)。Web 上 bundle 解析在 FCP 路径上: 4× CPU 节流下,Elk / AI Chat 曾测到约 +25% 到 +44%;小内容屏仍可保留 较薄收益。IFR 下应画出真正的外壳,不要因数据异步就跳过app.mount()(Hacker News 改 shell IFR 后,满速从约 +38% 回到 −12%)。
给开发者的建议
无论哪种画像:保持首屏确定性、副作用放进组合式 API 生命周期钩子(见编写 IFR 友好的首屏);enableElementTemplates: false 只用于二分定位疑似 ET 问题——它不是性能调节项。
这张表只是摘要。完整的测量数据——七 variant 策略阶梯、全示例逐项 FCP/TTI/体积、Lynx for Web 真实双线程与 ReactLynx 的对照、后续大应用复测、以及单线程裸 Web 基线分解——见专门的 IFR 性能数据页面。