You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WKWebView didFinish首次evaluateJavascript获取scrollHeight值异常

问题成因
  • WKNavigationDelegate的webView(_:didFinish:)回调触发时机,仅代表主框架的HTML资源、主脚本、主样式表的网络加载完成、DOM树初步构建完成,不代表页面完成重排(reflow)、所有异步资源(图片、自定义字体、懒加载内容)渲染完成、JS动态插入的节点布局完成。
  • WebKit采用渐进式渲染管线:DOM解析完成后会立刻做一次初步布局触发didFinish回调,此时如果存在未加载完成的大尺寸资源(比如未获取尺寸的高清图会临时按原始分辨率占位、未加载的自定义字体会导致文本按默认大尺寸排版)、JS在DOMContentLoaded后批量插入的DOM节点还未完成二次布局,此时计算的scrollHeight会包含临时占位的异常高度,就会出现数值远大于最终实际高度的情况。你遇到的20w+异常值基本都是未缩放的图片占位导致的。
  • 硬编码等待1.5秒能拿到正确值,本质是恰好等渲染管线跑完了所有布局流程,但该方案存在明显缺陷:不同性能设备的页面渲染耗时从几百毫秒到数秒不等,固定延迟要么导致不必要的等待,要么在低端设备上依然拿不到正确值;同时未指定执行Actor的Task会运行在全局并发线程池,而非主线程,违反了evaluateJavaScript必须在主线程调用的要求,才会触发Xcode的线程警告。
  • Safari调试时能看到正确高度,是因为手动打开调试器的耗时远大于页面渲染完成的时间,此时页面已经完成所有重排流程,自然展示最终正确值。
正确解决方案

禁止使用硬编码固定延迟的方案,采用基于原生布局回调/JS渲染完成通知的方式获取稳定高度,所有WebKit相关API调用必须保证在主线程执行。

方案1:KVO监听scrollView内容高度(无JS注入,稳定性最高)

WKWebView内部的scrollView.contentSize会在每次页面布局更新时自动刷新,我们可以在didFinish回调后启动KVO监听,当检测到高度连续两次一致(偏差小于1pt)时判定布局稳定,更新高度值后立刻移除监听,避免重复触发:

extension WebView {
    class Coordinator: NSObject, WKNavigationDelegate {
        var parent: WebView
        private var heightObservation: NSKeyValueObservation?
        private var lastRecordedHeight: CGFloat = 0

        init(_ parent: WebView) {
            self.parent = parent
        }

        public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            guard !webView.isLoading else { return }
            DispatchQueue.main.async { [weak self] in
                guard let self = self else { return }
                self.lastRecordedHeight = 0
                // 监听内容尺寸变化
                self.heightObservation = webView.scrollView.observe(\.contentSize, options: .new) { [weak self] scrollView, change in
                    guard let self = self, let newHeight = change.newValue?.height, newHeight > 0 else { return }
                    // 高度稳定后更新值
                    if abs(newHeight - self.lastRecordedHeight) < 1 {
                        self.parent.dynamicHeight = newHeight
                        // 移除监听避免重复触发
                        self.heightObservation?.invalidate()
                        self.heightObservation = nil
                        return
                    }
                    self.lastRecordedHeight = newHeight
                }
            }
        }

        // 新导航开始时清理旧监听,避免内存泄漏
        func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
            heightObservation?.invalidate()
            heightObservation = nil
            lastRecordedHeight = 0
        }
    }
}

该方案完全基于WebKit原生布局回调,不需要注入JS,没有额外等待耗时,布局完成后立刻拿到准确高度,适配所有类型的网页内容。

方案2:JS侧主动通知渲染完成(适用于自定义可控的HTML页面)

如果加载的是自定义传入的HTML内容,可以直接在JS侧监听所有资源加载完成,等两帧渲染确保布局稳定后,通过ScriptMessageHandler把高度主动推给原生:

  1. 提前给WKWebView注入监听脚本:
let heightDetectJS = """
window.addEventListener('load', () => {
    // 连续等待两个渲染帧,确保重排完成
    requestAnimationFrame(() => {
        requestAnimationFrame(() => {
            // 兼容不同CSS设置,取所有高度属性的最大值
            const finalHeight = Math.max(
                document.body.scrollHeight,
                document.documentElement.scrollHeight,
                document.body.offsetHeight,
                document.documentElement.offsetHeight
            );
            // 向原生发送高度消息
            window.webkit.messageHandlers.heightNotify.postMessage(finalHeight);
        })
    })
})
"""
let script = WKUserScript(source: heightDetectJS, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
webView.configuration.userContentController.addUserScript(script)
  1. 让Coordinator实现WKScriptMessageHandler协议,收到heightNotify消息时在主线程更新dynamicHeight即可。

注意事项

  • 所有WKWebView、evaluateJavaScript、scrollView相关的UI操作必须放在主线程执行,如果使用Swift Concurrency,需要用@MainActor或await MainActor.run包裹相关代码,避免线程警告。
  • 不要仅读取document.body.scrollHeight作为最终高度,部分页面的CSS会设置body高度为100%、修改overflow属性,会导致body的scrollHeight计算异常,建议同时读取documentElement和body的多个高度属性取最大值,过滤异常结果。

内容的提问来源于stack exchange,提问作者alyx

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 04:51:25