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

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

原文链接(查看演示视频更友好): https://github.com/xiaoxiaojx/blog/issues/37

近期 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 不会被计数进去,相关代码实现见

// https://github.com/WebKit/webkit/blob/main/Source/WebCore/page/FrameView.h#L975

static const unsigned visualCharacterThreshold = 200;

// 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 进行刷新缓存实时渲染,经过一周多的测试,白屏时间明显减少甚至不见!

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>`

把服务端渲染 SSR 改为现在的预渲染+接口聚合还有什么其他优势吗?

新的预渲染+接口聚合架构在公司 CDN 基建支持的情况下,不就是天然的 ⚡️ ESR (边缘流式渲染方案) 么 ~


4 回复

牛逼,mark 一下,万一用得上

iOS 哪天明显比安卓慢,就能用上了

针对您提到的服务端流式渲染在iOS中遇到的坑,这里提供一些可能的解决方案和见解。

首先,需要明确的是,iOS的WebView(如WKWebView)并不支持传统的流式渲染(分块渲染)。这意味着,即使服务端以流式的方式发送HTML内容,iOS的WebView也会等待整个HTML内容加载完成后再进行渲染。这可能会导致白屏时间延长,影响用户体验。

为了解决这个问题,可以尝试以下策略:

  1. 预渲染+接口聚合:将后端接口延时高的h5项目由SSR改为预渲染。即Node在接收到html请求时,先返回构建时生成的骨架屏html主体,然后再异步请求后端接口数据,获取到数据后再追加到html响应流中。这样Node能够在1ms内响应实际内容,让用户先看到页面框架。但需要注意的是,这种方式需要确保返回的html内容中有足够的有效字符(非空白字符)以触发iOS WebView的渲染。
  2. 代码示例
// 示例:在html响应流中插入足够的有效字符以触发iOS WebView渲染
const IOS_200 = `<div style="height:0;width:0;">\u200b\u200b\u200b\u200b\u200b\u200b...(此处省略200个\u200b字符)</div>`;
// 将IOS_200插入到html响应流的适当位置
  1. 优化后端接口:尽可能优化后端接口的性能,减少响应时间,从而缩短整个页面的加载时间。

  2. 监控和调试:使用工具监控页面的加载性能,确保流式渲染带来的性能提升超过实现的复杂性。同时,对iOS WebView的渲染行为进行调试,以发现并解决可能的问题。

希望这些建议能对您有所帮助!

回到顶部