本文同步发表于 微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新

一、预解析与预连接(域名级优化)

1. 原理

通过对目标域名提前进行DNS解析和TCP连接建立,减少页面加载时的网络延迟。

2. 核心API

webview.WebviewController.prepareForPageLoad(
    url: string,      // 要预连接的URL
    preconnect: boolean, // true: 预连接;false: 仅DNS预解析
    socketCount: number  // 预连接socket数量,最多6个
)

3. 使用示例

场景一:在Web组件出现时预连接
import { webview } from '@kit.ArkWeb';

@Entry
@Component
struct WebComponent {
    webviewController: webview.WebviewController = new webview.WebviewController();
    
    build() {
        Column() {
            Web({ 
                src: 'https://www.example.com/', 
                controller: this.webviewController 
            })
            .onAppear(() => {
                // 预连接:DNS解析 + TCP连接建立
                webview.WebviewController.prepareForPageLoad(
                    'https://www.example.com/', 
                    true,  // 进行预连接
                    2      // 预连接2个socket
                );
            })
        }
    }
}
场景二:在Ability中提前预连接首页
import { webview } from '@kit.ArkWeb';
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';

export default class EntryAbility extends UIAbility {
    onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
        console.info("EntryAbility onCreate");
        
        // 1. 提前初始化Web内核
        webview.WebviewController.initializeWebEngine();
        
        // 2. 预连接首页
        webview.WebviewController.prepareForPageLoad(
            "https://www.example.com/", 
            true,  // 预连接
            2      // 预连接2个socket
        );
        
        console.info("EntryAbility onCreate done");
    }
}

二、预加载(资源级优化)

1. 原理

提前下载页面所需的所有资源(主资源+子资源),避免阻塞页面渲染,但不执行JavaScript代码。

2. 核心API

webviewController.prefetchPage(url: string)

3. 使用示例

import { webview } from '@kit.ArkWeb';

@Entry
@Component
struct WebComponent {
    webviewController: webview.WebviewController = new webview.WebviewController();
    
    build() {
        Column() {
            Web({ 
                src: 'https://www.example.com/', 
                controller: this.webviewController 
            })
            .onPageEnd(() => {
                // 当前页面加载完成后,预加载下一个页面
                this.webviewController.prefetchPage(
                    'https://www.iana.org/help/example-domains'
                );
            })
        }
    }
}

三、预获取POST请求(请求级优化)

1. 核心API

// 预获取资源
webview.WebviewController.prefetchResource(
    request: RequestInfo,      // 请求信息
    headers?: Header[],       // 请求头
    key?: string,             // 资源标识键
    expirationTime?: number   // 过期时间(秒)
)

// 清除预获取资源
webview.WebviewController.clearPrefetchedResource(keys: string[])

2. 使用场景

场景一:页面加载时预获取POST请求
@Entry
@Component
struct WebComponent {
    webviewController: webview.WebviewController = new webview.WebviewController();
    
    build() {
        Column() {
            Web({ src: 'https://www.example.com/', controller: this.webviewController })
                .onAppear(() => {
                    // 预获取POST请求
                    webview.WebviewController.prefetchResource(
                        {
                            url: 'https://www.example1.com/post?e=f&g=h',
                            method: 'POST',
                            formData: 'a=x&b=y',
                        },
                        [{
                            headerKey: 'c',
                            headerValue: 'z',
                        }],
                        'KeyX',  // 资源标识
                        500      // 500秒后过期
                    );
                })
                .onPageEnd(() => {
                    // 清除不再使用的预获取资源
                    webview.WebviewController.clearPrefetchedResource(['KeyX']);
                })
        }
    }
}
场景二:在Ability中预获取首页POST请求
export default class EntryAbility extends UIAbility {
    onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
        webview.WebviewController.initializeWebEngine();
        
        webview.WebviewController.prefetchResource(
            {
                url: "https://www.example1.com/post?e=f&g=h",
                method: "POST",
                formData: "a=x&b=y",
            },
            [{
                headerKey: "c",
                headerValue: "z",
            }],
            "KeyX",
            500
        );
    }
}

四、预编译生成编译缓存

1. 原理

提前将JavaScript脚本编译成字节码缓存,减少运行时编译开销。

2. 核心API

webviewController.precompileJavaScript(
    url: string,           // 脚本URL
    script: Uint8Array,    // 脚本内容
    options?: CacheOptions // 缓存选项
)

3. 实现示例

步骤1:Ability中存储UIContext
// EntryAbility.ets
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';

const localStorage: LocalStorage = new LocalStorage('uiContext');

export default class EntryAbility extends UIAbility {
    storage: LocalStorage = localStorage;
    
    onWindowStageCreate(windowStage: window.WindowStage) {
        windowStage.loadContent('pages/Index', this.storage, (err, data) => {
            if (err.code) {
                return;
            }
            // 存储UIContext供后续使用
            this.storage.setOrCreate<UIContext>(
                "uiContext", 
                windowStage.getMainWindowSync().getUIContext()
            );
        });
    }
}
步骤2:动态组件代码
// DynamicComponent.ets
import { NodeController, BuilderNode, FrameNode, UIContext } from '@kit.ArkUI';

export interface BuilderData {
    url: string;
    controller: WebviewController;
    context: UIContext;
}

export class NodeControllerImpl extends NodeController {
    private rootNode: BuilderNode<BuilderData[]> | null = null;
    private wrappedBuilder: WrappedBuilder<BuilderData[]> | null = null;
    
    constructor(wrappedBuilder: WrappedBuilder<BuilderData[]>, context: UIContext) {
        super();
        this.wrappedBuilder = wrappedBuilder;
    }
    
    makeNode(): FrameNode | null {
        return this.rootNode?.getFrameNode() || null;
    }
    
    initWeb(url: string, controller: WebviewController) {
        // 初始化BuilderNode
        this.rootNode = new BuilderNode(uiContext);
        this.rootNode.build(this.wrappedBuilder, { url, controller });
    }
}


export const createNode = (wrappedBuilder: WrappedBuilder<BuilderData[]>, data: BuilderData) => {
  const baseNode = new NodeControllerImpl(wrappedBuilder, data.context);
  baseNode.initWeb(data.url, data.controller);
  return baseNode;
}
步骤3:预编译组件
// PrecompileWebview.ets
import { BuilderData } from './DynamicComponent';
import { Config, configs } from './PrecompileConfig';

@Builder
function webBuilder(data: BuilderData) {
    Web({ src: data.url, controller: data.controller })
        .onControllerAttached(() => {
            precompile(data.controller, configs, data.context);
        })
        .fileAccess(true)  // 允许访问本地文件
}

export const precompileWebview = wrapBuilder<BuilderData[]>(webBuilder);

// 预编译函数
export const precompile = async (
    controller: WebviewController, 
    configs: Array<Config>, 
    context: UIContext
) => {
    for (const config of configs) {
        // 从rawfile读取本地JS文件
        let content = await readRawFile(config.localPath, context);
        
        try {
            // 执行预编译
            await controller.precompileJavaScript(
                config.url, 
                content, 
                config.options
            );
            console.error('precompile successfully!');
        } catch (err) {
            console.error('precompile failed: ' + err);
        }
    }
}
步骤4:资源配置
// PrecompileConfig.ets
import { webview } from '@kit.ArkWeb'

export interface Config {
    url: string,           // 资源URL
    localPath: string,     // 本地资源路径
    options: webview.CacheOptions
}

export let configs: Config[] = [
    {
        url: 'https://www.example.com/example.js',
        localPath: 'example.js',
        options: {
            responseHeaders: [
                { headerKey: 'E-Tag', headerValue: 'aWO42N9P9dG/5xqYQCxsx+vDOoU=' },
                { headerKey: 'Last-Modified', headerValue: 'Wed, 21 Mar 2025 10:38:41 GMT' }
            ]
        }
    }
]
步骤5:业务Web组件
// BusinessWebview.ets
import { BuilderData } from './DynamicComponent';

@Builder
function webBuilder(data: BuilderData) {
    Web({ src: data.url, controller: data.controller })
        .cacheMode(CacheMode.Default)  // 使用默认缓存模式
}

export const businessWebview = wrapBuilder<BuilderData[]>(webBuilder);
步骤6:页面中使用
// Index.ets
import { webview } from '@kit.ArkWeb';
import { NodeController } from '@kit.ArkUI';
import { createNode } from './DynamicComponent';
import { precompileWebview } from './PrecompileWebview';
import { businessWebview } from './BusinessWebview';

@Entry
@Component
struct Index {
    @State precompileNode: NodeController | undefined = undefined;
    precompileController: webview.WebviewController = new webview.WebviewController();
    
    @State businessNode: NodeController | undefined = undefined;
    businessController: webview.WebviewController = new webview.WebviewController();
    
    aboutToAppear(): void {
        // 初始化预编译组件
        this.precompileNode = createNode(precompileWebview, {
            url: 'https://www.example.com/empty.html',
            controller: this.precompileController,
            context: this.getUIContext()
        });
    }
    
    build() {
        Column() {
            // 点击按钮加载业务页面
            Button('加载页面')
                .onClick(() => {
                    this.businessNode = createNode(businessWebview, {
                        url: 'https://www.example.com/business.html',
                        controller: this.businessController,
                        context: this.getUIContext()
                    });
                })
            
            // 显示业务Web组件
            NodeContainer(this.businessNode);
        }
    }
}

4. 缓存更新机制

要更新已生成的编译字节码,只需修改CacheOptionsresponseHeadersE-TagLast-Modified值,再次调用precompileJavaScript接口即可。

五、离线资源免拦截注入

1. 原理

将图片、样式表、脚本等资源提前注入到内核内存缓存中,避免网络请求。

2. 核心API

webviewController.injectOfflineResources(
    resources: OfflineResourceMap[]
)

3. 实现示例

步骤1:资源配置
// Resource.ets
import { webview } from '@kit.ArkWeb';

export interface ResourceConfig {
    urlList: Array<string>,           // 资源URL列表
    type: webview.OfflineResourceType, // 资源类型
    responseHeaders: Array<Header>,    // 响应头
    localPath: string                  // 本地资源路径
}

export const resourceConfigs: ResourceConfig[] = [
    {
        localPath: 'example.png',
        urlList: [
            'https://www.example.com/',
            'https://www.example.com/path1/example.png',
            'https://www.example.com/path2/example.png',
        ],
        type: webview.OfflineResourceType.IMAGE,
        responseHeaders: [
            { headerKey: 'Cache-Control', headerValue: 'max-age=1000' },
            { headerKey: 'Content-Type', headerValue: 'image/png' },
        ]
    },
    {
        localPath: 'example.js',
        urlList: ['https://www.example.com/example.js'],
        type: webview.OfflineResourceType.CLASSIC_JS,
        responseHeaders: [
            { headerKey: 'Cross-Origin', headerValue: 'anonymous' }
        ]
    }
];
步骤2:资源注入组件
// InjectWebview.ets
import { webview } from '@kit.ArkWeb';
import { resourceConfigs } from './Resource';
import { BuilderData } from './DynamicComponent';

@Builder
function webBuilder(data: BuilderData) {
    Web({ src: data.url, controller: data.controller })
        .onControllerAttached(async () => {
            try {
                // 注入离线资源
                data.controller.injectOfflineResources(
                    await getData(data.context)
                );
            } catch (err) {
                console.error('error: ' + err.code + ' ' + err.message);
            }
        })
        .fileAccess(true)
}

export const injectWebview = wrapBuilder<BuilderData[]>(webBuilder);

export async function getData(context: UIContext) {
    const resourceMapArr: webview.OfflineResourceMap[] = [];
    
    for (let config of resourceConfigs) {
        let buf: Uint8Array = new Uint8Array(0);
        if (config.localPath) {
            // 读取本地文件
            buf = await readRawFile(config.localPath, context);
        }
        
        resourceMapArr.push({
            urlList: config.urlList,
            resource: buf,
            responseHeaders: config.responseHeaders,
            type: config.type,
        });
    }
    
    return resourceMapArr;
}

六、技术对比

优化技术 优化级别 适用场景 执行时机 资源消耗
预解析预连接 域名级 首次访问、页面跳转 页面加载前
预加载 资源级 可预测的下一页 当前页面完成后
预获取POST 请求级 表单提交、API调用 页面加载前后均可
预编译缓存 脚本级 JS密集型页面 应用启动时 中高
离线资源注入 资源级 静态资源较多页面 应用启动时

七、建议

1. 分层优化

  • 第一层:对首页使用prepareForPageLoad()进行预连接

  • 第二层:对核心JS使用precompileJavaScript()预编译

  • 第三层:对静态资源使用injectOfflineResources()注入

  • 第四层:对下一页使用prefetchPage()预加载

2. 内存管理

  • 及时使用clearPrefetchedResource()清理不再需要的预获取资源

  • 监控内存使用,避免过度预加载导致内存压力

3. 网络环境适配

  • 在Wi-Fi环境下更激进地使用预加载和预获取

  • 在移动网络下适当减少预加载量

从域名级到资源级的全方位加速方案:

  1. 预解析预连接:解决DNS查询和TCP握手延迟

  2. 预加载:提前获取页面资源,避免渲染阻塞

  3. 预获取POST请求:优化API调用性能

  4. 预编译缓存:减少JavaScript编译时间

  5. 离线资源注入:完全避免网络请求

通过合理组合使用这些技术,可以显著提升Web页面的加载速度,特别是在网络条件较差或页面资源较多的场景下。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐