服务端流式渲染 iOS 中踩坑记

近期 iOS 客户端反映 WebView 中打开 h5 页面存在明显的白屏时间, 于是打算把后端接口延时高(> 150ms)的 h5 项目由现在的 SSR 改成 html 请求达到 Node 时率先返回构建时生成的骨架屏 html 主体, 然后再异步请求后端接口数据, 获取到接口数据后再追加到 html 响应流中。这样 Node 能够 1ms 内响应实际内容让用户先看到页面框架, 通过内网并发聚合的接口数据也能让客户端直接复用这部分数据更快展示出最终屏。

按理来说 h5 不再受限于后端接口的响应时长, 能够第一时间渲染出骨架屏页面, 但是体验后白屏时间好像没怎么缩短? 最后反复删减代码测试发现了一个残酷的现实 👇

iOS WKWebView 不支持流式渲染(分块渲染), 安卓 WebView 与 PC Chrome 是支持的。

即表示 IOS 中会等待 html 请求彻底结束后才开始渲染, 如下是安卓与 IOS 中的效果演示视频,希望其他同学不要再踩坑 🤯

https://user-images.githubusercontent.com/23253540/174447134-25daa11b-0be8-4330-85b7-e464c14f6047.mp4

https://user-images.githubusercontent.com/23253540/174447157-8ccc2be4-52fe-4d67-a11d-d4701677aa5d.mp4


2022-06-20 更新,经过大佬提醒,IOS 中如果返回的 data 是普通文本文字,或返回的数据中包含普通文本文字,那只需要达到非空 200 字节即可以触发渲染,详细见 iOS之深入解析WKWebView加载的生命周期与代理方法

https://user-images.githubusercontent.com/23253540/174550696-cb3b54df-6db1-4aff-8adb-b60258461b20.mp4

所以 IOS chrome 与 safari 也是支持流式渲染(分块渲染),App 中没有效果是有效内容没有达到 200 字节 (innerText)

h5 页面首屏文字等内容达到 200+ 字节还是较少的,设置为 display: none 来凑数的 div 不会被计数进去,相关代码实现见
1
2
3
// https://github.com/WebKit/webkit/blob/main/Source/WebCore/page/FrameView.h#L975

static const unsigned visualCharacterThreshold = 200;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// https://github.com/WebKit/WebKit/blob/ed7fed17c5ac886890859f1fc8682dba06424616/Source/WebCore/page/FrameView.cpp#L4685

void FrameView::checkAndDispatchDidReachVisuallyNonEmptyState()
{
// ...
// The first few hundred characters rarely contain the interesting content of the page.
if (m_visuallyNonEmptyCharacterCount > visualCharacterThreshold)
return true;
}

void FrameView::incrementVisuallyNonEmptyCharacterCount(const String& inlineText)
{
if (m_visuallyNonEmptyCharacterCount > visualCharacterThreshold && m_hasReachedSignificantRenderedTextThreshold)
return;

auto nonWhitespaceLength = [](auto& inlineText) {
auto length = inlineText.length();
for (unsigned i = 0; i < inlineText.length(); ++i) {
if (isNotHTMLSpace(inlineText[i]))
continue;
--length;
}
return length;
};
m_visuallyNonEmptyCharacterCount += nonWhitespaceLength(inlineText);
++m_textRendererCountForVisuallyNonEmptyCharacters;
}

2022-06-26 更新,最后给 body 标签插入了一个塞了 200 个空格字符的 div 来强制 WKWebView 进行刷新缓存实时渲染,经过一周多的测试,白屏时间明显减少甚至不见 🎉

1
const IOS_200 = `<div style="height:0;width:0;">\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b</div>`