并行加载是 H5 即开 SDK 的加速技术,通过 Native 层在用户打开页面时并行请求关键资源(如 index.html 和 CSR 模式 API),利用 WebView 初始化的时间窗口提前发起请求,减少加载耗时。其核心挑战是解决 WebView 与并行任务间的资源无缝交接问题。

并行加载能力核心解析
什么是并行加载?
并行加载是 H5 即开 SDK 提供的一项加速能力。其核心逻辑是:在用户打开页面时,通过 Android 或 iOS 的 Native 层并行请求关键资源,减少页面加载耗时。本质是利用 WebView 及 Native 页面初始化的时间窗口,提前发起资源请求,实现“时间复用”。
核心加载资源
并行加载的关键资源包含两类:
- H5 页面首帧渲染依赖的
index.html(首帧资源,加载时机极早)。
- CSR 模式下页面依赖的 API 接口(通常在首帧渲染后调用,加载时机较晚)。
工作流程示意图
即开 SDK 在流程中主要完成两件事:
- 用户打开 URL 时,并行请求 H5 依赖的 API 接口或
index.html 文件。
- 拦截 WebView 请求,将并行加载的缓存数据导流给 WebView,实现加速。
并行加载的核心挑战:资源交接场景
并行加载的核心问题是如何在 WebView 需要资源时,将并行请求的资源无缝交接。根据资源状态可分为三类场景:
| 类型 |
描述 |
| 场景一 |
网络数据未响应,WebView 开始需要资源 |
| 场景二 |
网络数据已正确响应,网络数据流已建联,WebView 开始需要资源 |
| 场景三 |
网络数据返回错误,WebView 开始需要资源 |
下面我们针对三个场景分别看处理方案。
场景一:网络数据尚未响应,WebView开始需要资源
解决方案一:忽略网络数据,WebView 自己加载资源。
矛盾点:如果是因为服务器压力大、网络环境差导致的响应慢,WebView 自己去加载也会遇到同样的问题,而且直接放弃已并行加载的任务,等于浪费了已经处于建联中、可能已完成的部分等待时间。
解决方案二:WebView 等待网络资源加载成功后,再使用加载成功的资源。
矛盾点:让 WebView 的资源获取线程等待并行任务响应并返回,那么等多久?如果时间太久怎么办?需要设置超时时间,如何等待?并且在等待过程中还要监控并行任务是否已经返回。
场景二:网络数据已响应,WebView开始需要资源
场景二需要考虑两种情况:
- 情况一:在 WebView 需要数据的时候,网络数据流刚好完成建联,WebView 可以直接使用网络数据流加载。
- 情况二:网络数据流建联的时候,WebView 还未开始需要使用数据,并行任务有时间将网络数据流读取到缓冲区中,WebView 在需要数据的时候,可以读取缓冲区的数据。
矛盾点:预先进行网络的数据读取,再交接给 WebView,能更大程度上降低读取耗时。但是,如果并行任务在读取的过程中,还没有读完,WebView 就来要数据怎么办?让 WebView 等待吗?如果等待,等待多久?如果不等待,又如何将已读取在缓冲区中的数据和未读取的网络流数据一起交给 WebView 呢?
场景三:网络数据返回错误,WebView开始需要资源
解决方案:并行任务已失败,直接废弃并行任务,让 WebView 自己加载资源。
早期方案设计与局限
最初我们希望并行任务设计得足够简单,基于对所有场景的理解并权衡开发难度,设计了第一个方案。
我们决定避开场景二中“网络流读取到一半 WebView 来取数据”的复杂场景,将复杂的生产者消费者模式简化为典型的模式:消费者不能打断生产者的生产过程,而是等待生产完成,避免中间态的复杂处理。我们根据资源特性,将 index.html 任务和 CSR 模式下的 API 任务分为两种不同的方式处理。
index.html 首帧资源处理
index.html 是 WebView 完成初始化后第一个要加载的资源(首帧资源),其可用并行加载时间窗口大约只有 100ms。我们认为在这个时间内,并行任务大概率可以完成建联,但可能没有时间完成数据流的全量读取。因此,我们使用 stream 对象来保存网络数据流。
方案逻辑如下:
1. index 并行任务发起
flowchart TD
A[用户点击H5入口按钮] --> B[并行发起index任务]
B --> C[native访问网络开始建联]
C --> D{建联成功?}
D -->|是| E[保存网络Stream对象]
E -->F[记录结束状态]
D -->|否| F[记录结束状态]

2. WebView 使用 index 资源
flowchart TD
A[用户点击H5入口按钮] --> B[webview初始化]
B --> C{需要使用index数据流}
C -->|是| D[查找stream对象是否存在]
D -->|存在| E[直接使用数据流]
D -->|不存在| F{网络访问已结束?}
F -->|是且stream为空| G[判定并行任务失败]
F -->|否| H[进入等待模式]
H --> I[建立探测循环体]
I --> J[每隔5ms探测]
J --> K{stream存在?}
K -->|是| L[使用数据流并退出循环]
K -->|否| M{并行任务已结束?}
M -->|是| N[放弃并行任务并退出循环]
M -->|否| O{达到1500ms?}
O -->|是| P[超时退出循环]
O -->|否| J
P --> Q[webview自主加载资源]
N --> Q
L --> R[流程结束]
G --> Q
Q --> R

循环等待机制(5ms探测,1500ms超时)实现了轻量的线程同步。
API 接口资源处理
API 接口通常在页面完成首帧渲染后才调用,此时并行任务有较充足的时间来完成网络建联甚至将网络流全量读取到缓冲区。针对这种情况,我们保存一个 byte 数组,将网络流数据全量读取到这个数组中,WebView 使用时将其包装成 ByteArrayInputStream 返回。
逻辑图如下:
1. API 并行任务发起
flowchart TD
A[用户点击H5入口按钮] --> B[并行发起index任务]
B --> C[native访问网络开始建联]
C --> D{建联成功?}
D -->|是| E[读取网络stream]
E --> F[保存到本地byte数组]
D -->|否| G[记录结束状态]
F --> H[流程完成]
G --> H

2. WebView 使用 API 资源
flowchart TD
A[用户点击H5入口按钮] --> B[webview初始化]
B --> C{需要使用index数据流}
C -->|是| D[查找byte数组是否存在]
D -->|存在| E[封装为ByteArrayInputStream返回给webview]
E --> X[流程结束]
D -->|不存在| G{网络访问已结束?}
G -->|是且byte数组为空| H[判定并行任务失败]
G -->|否| I[进入等待模式]
I --> J[建立探测循环体]
J --> K[每隔5ms探测]
K --> L{byte数组存在?}
L -->|是| M[封装使用并退出循环]
L -->|否| N{并行任务已结束?}
N -->|是| O[放弃并行任务并退出]
N -->|否| P{达到1500ms?}
P -->|是| Q[超时退出]
P -->|否| K
M --> E
O --> R[webview自主加载]
Q --> R
H --> R
R --> X

早期方案的核心局限:
- 时间浪费:循环探测(每 5ms 一次)存在无效等待,最坏情况有近5ms的延迟。
- 内存浪费:API 接口采用全量缓存,占用额外内存。
- 资源利用率低:放弃了“读取一半”的中间态交接,未充分利用并行请求的时间窗口,且在等待全量缓存完成时造成了双重时间浪费。
方案演进:优化时间与内存消耗
新方案的优化侧重点是消除循环等待和全量缓存。核心思路是:解决中间态交接问题,通过线程同步和流桥接技术实现优化。
- 干掉循环等待:用线程同步机制(
wait/notify)替代轮询。
- 干掉全量缓存:采用半缓冲模式,仅缓存部分数据。
- 支持中间态交接:允许 WebView 打断并行任务,合并已缓冲与未缓冲数据。
技术实现:生产者-消费者模型
我们把网络建联和读取数据到缓冲区的过程看作生产,将这个过程建模:
- 生产者接到生产产品任务。
- 消费者随时过来消费产品。
- 若生产者未开始,消费者可等待(超时放弃)。
- 若生产者正在生产,消费者可随时打断,拿走“半成品”并完成剩余生产。
- 若生产者已完成,消费者直接拿走全部产品。
这涉及到两个关键技术点:
- 生产者在生产过程中可随时被打断。
- 生产了一半的产品可被直接使用。
我们尝试使用Java多线程的同步机制和桥接流技术来实现。
代码核心实现(第一版:基于 synchronized)
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import static java.lang.Thread.sleep;
public class Main2 {
// 模拟响应体
public static class ResponseBody {
InputStream stream;
public ResponseBody(InputStream stream) {
this.stream = stream;
}
public InputStream byteStream() {
return stream;
}
}
public static class SyncLoadResponseBean {
private static final String TAG = "SyncLoadResponseBean";
// 状态常量定义
public static final int INIT = 1; // 初始状态
public static final int READY = 2; // 数据准备就绪
public static final int OFFER = 3; // 数据已提供
public static final int DROP = 4; // 数据已丢弃
private final String mRequestUrl;
private final ConcurrentHashMap<String, SyncLoadResponseBean> mSyncLoadCache;
private final AtomicInteger mStatus = new AtomicInteger(INIT); // 状态机
// 数据存储相关
private Map<String, String> mResponseHeader;
private ByteArrayOutputStream mBufferStream;
private InputStream mNetworkStream;
private long mResponseTime;
public SyncLoadResponseBean(String requestUrl, ConcurrentHashMap<String, SyncLoadResponseBean> syncLoadCache) {
mRequestUrl = requestUrl;
mSyncLoadCache = syncLoadCache;
mStatus.set(INIT);
}
public boolean before(int status) {
return mStatus.get() < status;
}
public boolean during(int status) {
return mStatus.get() == status;
}
public boolean after(int status) {
return mStatus.get() >= status;
}
/**
* 唤醒所有等待线程
*/
public void signalAll() {
synchronized (SyncLoadResponseBean.this) {
this.notifyAll();
}
}
/**
* 保存响应数据并预处理
*/
public void saveResponse(ResponseBody responseBody, Map<String, String> responseHeader) {
streamReady(responseBody, responseHeader);
preReadStream();
}
/**
* 准备数据流
*/
private void streamReady(ResponseBody responseBody, Map<String, String> responseHeader) {
synchronized (SyncLoadResponseBean.this) {
TLog.d(TAG, "并行加载 响应保存");
mResponseTime = System.currentTimeMillis();
mResponseHeader = responseHeader;
mBufferStream = new ByteArrayOutputStream();
if (responseBody != null) {
mNetworkStream = responseBody.byteStream();
// 根据流是否有效设置状态
if (mNetworkStream != null) {
mStatus.set(READY);
} else {
drop();
}
} else {
drop();
}
TLog.d(TAG, "并行加载 保存完成 通知消费者");
signalAll();
}
}
private void preReadStream() {
TLog.d(TAG, "并行加载 预读缓存 开始");
byte[] buffer = new byte[4096];
int num = 0;
try {
while (during(READY)) {
synchronized (SyncLoadResponseBean.this) {
try {
// 双重校验锁
if (during(READY)) {
// 读取网络流数据
int bytesRead = mNetworkStream.read(buffer);
if (bytesRead == -1) {
TLog.d(TAG, "并行加载 预读缓存 完成 " + bytesRead);
closeStream(mNetworkStream);
mNetworkStream = null;
return;
}
TLog.d(TAG, "并行加载 预读缓存 " + bytesRead);
mBufferStream.write(buffer, 0, bytesRead);
}
} finally {
num++;
TLog.d(TAG, "并行加载 预读缓存 第" + num + "次通知消费者");
signalAll();
}
}
}
// 已经提供了数据,则打印日志
if (after(OFFER)) {
TLog.d(TAG, "并行加载 数据流已提供 预读缓存 关闭");
}
} catch (IOException e) {
TLog.e(TAG, "并行加载 预读缓存 异常 关闭", e);
synchronized (SyncLoadResponseBean.this) {
// 在读取过程中出现异常,但未提供数据,则丢弃
if (!after(OFFER)) {
drop();
}
}
}
}
/**
* 获取桥接流
* WebView线程调用该方法获取数据流
*/
public InputStream getBridgedStream() {
TLog.d(TAG, "并行加载 查找流数据");
synchronized (SyncLoadResponseBean.this) {
try {
if (before(READY)) {
TLog.d(TAG, "并行加载 查找流数据 进入等待状态");
this.wait(5000);
TLog.d(TAG, "并行加载 查找流数据 被唤醒");
}
// 等待结束,再确认一次状态是否可用
if (before(READY)) {
TLog.d(TAG, "并行加载 查找流数据 依旧没有可用数据 返回空流");
drop();
return null;
} else if (after(OFFER)) {
TLog.d(TAG, "并行加载 查找流数据 数据已被废弃或者被他人被使用 返回空流");
return null;
} else if (isTimeOut()) {
TLog.d(TAG, "并行加载 查找流数据 数据超时 返回空流");
drop();
return null;
} else {
if (mNetworkStream != null && mBufferStream != null) {
mStatus.set(OFFER);
// 创建桥接流,包含已缓存数据和剩余网络流
ByteArrayInputStream cachedStream = new ByteArrayInputStream(mBufferStream.toByteArray());
TLog.d(TAG, "并行加载 查找流数据 返回桥接流");
return new SequenceInputStream(cachedStream, mNetworkStream);
} else if (mNetworkStream != null) {
mStatus.set(OFFER);
TLog.d(TAG, "并行加载 查找流数据 返回网络流");
return mNetworkStream;
} else if (mBufferStream != null) {
mStatus.set(OFFER);
TLog.d(TAG, "并行加载 查找流数据 返回缓存流");
return new ByteArrayInputStream(mBufferStream.toByteArray());
} else {
drop();
TLog.d(TAG, "并行加载 查找流数据 返回空流");
return null;
}
}
} catch (Exception e) {
TLog.e("TAG", "Create bridged stream failed", e);
drop();
return null;
}
}
}
/**
* 获取响应头
*/
public Map<String, String> getResponseHeader() {
// 如果请求里面不带跨域标识,则带上跨域标识
if (mResponseHeader != null && !mResponseHeader.containsKey("Access-Control-Allow-Origin")) {
mResponseHeader.put("Access-Control-Allow-Origin", "*");
}
return mResponseHeader;
}
/**
* 统一关闭流资源操作
*/
private void closeStream(Closeable stream) {
if (stream != null) {
try {
stream.close();
} catch (Exception e) {
TLog.e(TAG, "关闭流失败", e);
}
}
}
/**
* 判断数据有没有超时
*/
private boolean isTimeOut() {
return Math.abs(mResponseTime - System.currentTimeMillis()) > 5000;
}
/**
* 丢弃数据
*/
public void drop() {
synchronized (SyncLoadResponseBean.this) {
mStatus.set(DROP);
mResponseHeader = null;
mResponseTime = 0;
closeStream(mBufferStream);
closeStream(mNetworkStream);
mBufferStream = null;
mNetworkStream = null;
mSyncLoadCache.remove(mRequestUrl);
TLog.d(TAG, "并行加速 缓存数据丢弃");
}
}
}
/**
* 主方法,测试程序入口
*/
public static void main(String[] args) {
SyncLoadResponseBean syncLoadResponseBean = new SyncLoadResponseBean("https://www.baidu.com", new ConcurrentHashMap<>());
// 模拟生产者线程
new Thread(() -> {
try {
System.out.println("生产者启动");
// 模拟网络建联
sleep(200);
// 模拟网络资源返回
File file = new File("./xxxx.txt");
ResponseBody responseBody = new ResponseBody(new FileInputStream(file));
// 模拟响应
syncLoadResponseBean.saveResponse(responseBody, new HashMap<>());
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
System.out.println("生产线程工作完成,唤醒等待中的消费者");
syncLoadResponseBean.signalAll();
}
}).start();
// 模拟消费者线程
new Thread(() -> {
System.out.println("消费者启动");
// 模拟读取
InputStream stream = syncLoadResponseBean.getBridgedStream();
System.out.println("消费者 " + stream);
}).start();
}
}
代码解读与问题发现
代码工作流程:
- 生产者(网络线程)发起数据请求。
- 建联后首次
notify,尝试唤醒等待中的消费者(WebView线程)。
- 若无消费者等待,则开始预读。预读是循环读取过程,每次循环(读取4096字节)都会经历加锁->读取->释放锁->
notify,保证消费者可随时打断并拿走“半成品”。
- 使用
synchronized 实现线程间协同,AtomicInteger 实现状态同步,wait(timeout) 实现超时等待。
- 使用
SequenceInputStream 桥接已读缓冲流与未读网络流。
然而,运行测试时发现了问题:
生产者启动
消费者启动
并行加载 查找流数据
并行加载 查找流数据 进入等待状态
并行加载 响应保存
并行加载 保存完成 通知消费者
并行加载 预读缓存 开始
并行加载 预读缓存 4096
并行加载 预读缓存 第1次通知消费者
... (多次预读和通知)
并行加载 预读缓存 第362次通知消费者
并行加载 查找流数据 被唤醒
并行加载 查找流数据 返回桥接流
并行加载 预读缓存 第363次通知消费者
并行加载 数据流已提供 预读缓存 关闭
消费者 java.io.SequenceInputStream@696759a5
问题:消费者在生产者第363次 notify 时才被唤醒,唤醒时机随机且延迟严重。这是因为 synchronized 使用的是非公平锁,生产者释放锁并唤醒消费者后,锁很可能又被生产者自己(预读任务)重新抢走,导致消费者饥饿。
方案演进:优化同步策略(使用公平锁)
为了解决非公平锁导致的唤醒延迟问题,我们引入公平锁机制。在 Java多线程 编程中,ReentrantLock 可以创建公平锁,结合 Condition 实现可预测的线程等待/唤醒。
公平锁代码实现
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import static java.lang.Thread.sleep;
public class Main {
// ResponseBody 类同上,略...
public static class SyncLoadResponseBean {
private static final String TAG = "SyncLoadResponseBean";
// 状态常量定义同上,略...
private final String mRequestUrl;
private final ConcurrentHashMap<String, SyncLoadResponseBean> mSyncLoadCache;
private final ReentrantLock mLock = new ReentrantLock(true); // 公平锁
private final Condition mCondition = mLock.newCondition(); // 条件变量
private final AtomicInteger mStatus = new AtomicInteger(INIT);
// 数据存储相关同上,略...
public SyncLoadResponseBean(String requestUrl, ConcurrentHashMap<String, SyncLoadResponseBean> syncLoadCache) {
mRequestUrl = requestUrl;
mSyncLoadCache = syncLoadCache;
mStatus.set(INIT);
}
// before, during, after 方法同上,略...
/**
* 唤醒所有等待线程
*/
public void signalAll() {
if (mLock.tryLock()) {
try {
mCondition.signalAll();
} finally {
mLock.unlock();
}
}
}
/**
* 保存响应数据并预处理
*/
public void saveResponse(ResponseBody responseBody, Map<String, String> responseHeader) {
streamReady(responseBody, responseHeader);
preReadStream();
}
/**
* 准备数据流
*/
private void streamReady(ResponseBody responseBody, Map<String, String> responseHeader) {
mLock.lock();
try {
TLog.d(TAG, "并行加载 响应保存");
mResponseTime = System.currentTimeMillis();
mResponseHeader = responseHeader;
mBufferStream = new ByteArrayOutputStream();
if (responseBody != null) {
mNetworkStream = responseBody.byteStream();
if (mNetworkStream != null) {
mStatus.set(READY);
} else {
drop();
}
} else {
drop();
}
} finally {
TLog.d(TAG, "并行加载 保存完成 通知消费者");
mCondition.signalAll();
mLock.unlock();
}
}
private void preReadStream() {
byte[] buffer = new byte[4096];
int num = 0;
try {
while (during(READY)) {
mLock.lock();
try {
// 双重校验锁
if (during(READY)) {
int bytesRead = mNetworkStream.read(buffer);
if (bytesRead == -1) {
TLog.d(TAG, "并行加载 预读缓存 完成 " + bytesRead);
closeStream(mNetworkStream);
mNetworkStream = null;
break;
}
TLog.d(TAG, "并行加载 预读缓存 " + bytesRead);
mBufferStream.write(buffer, 0, bytesRead);
}
} finally {
num++;
TLog.d(TAG, "并行加载 预读缓存 第" + num + "次通知消费者");
mCondition.signalAll();
mLock.unlock();
}
}
if (after(OFFER)) {
TLog.d(TAG, “并行加载 数据流已提供 预读缓存 关闭");
}
} catch (IOException e) {
TLog.e(TAG, "并行加载 预读缓存 异常 关闭", e);
mLock.lock();
try {
if (!after(OFFER)) {
drop();
}
} finally {
mLock.unlock();
}
}
}
/**
* 获取桥接流
*/
public InputStream getBridgedStream() {
mLock.lock();
TLog.d(TAG, "并行加载 查找流数据");
try {
if (before(READY)) {
long time1 = System.currentTimeMillis();
TLog.d(TAG, "并行加载 查找流数据 进入等待状态");
mCondition.await(5, TimeUnit.SECONDS);
long time2 = System.currentTimeMillis();
TLog.d(TAG, "并行加载 查找流数据 被唤醒 等待时长:" + (time2 - time1));
}
// 等待结束,再确认一次状态是否可用
if (before(READY)) {
TLog.d(TAG, "并行加载 查找流数据 依旧没有可用数据 返回空流");
drop();
return null;
} else if (after(OFFER)) {
TLog.d(TAG, "并行加载 查找流数据 数据已被废弃或者被他人被使用 返回空流");
return null;
} else if (isTimeOut()) {
TLog.d(TAG, "并行加载 查找流数据 数据超时 返回空流");
drop();
return null;
} else {
if (mNetworkStream != null && mBufferStream != null) {
mStatus.set(OFFER);
ByteArrayInputStream cachedStream = new ByteArrayInputStream(mBufferStream.toByteArray());
TLog.d(TAG, "并行加载 查找流数据 返回桥接流");
return new SequenceInputStream(cachedStream, mNetworkStream);
} else if (mNetworkStream != null) {
mStatus.set(OFFER);
TLog.d(TAG, "并行加载 查找流数据 返回网络流");
return mNetworkStream;
} else if (mBufferStream != null) {
mStatus.set(OFFER);
TLog.d(TAG, "并行加载 查找流数据 返回缓存流");
return new ByteArrayInputStream(mBufferStream.toByteArray());
} else {
drop();
TLog.d(TAG, "并行加载 查找流数据 返回空流");
return null;
}
}
} catch (Exception e) {
TLog.e("TAG", "Create bridged stream failed", e);
drop();
return null;
} finally {
mLock.unlock();
}
}
// getResponseHeader, closeStream, isTimeOut, drop 方法(内部使用mLock)略...
}
// main 测试方法同上,略...
}
运行结果与桥接流解读
使用公平锁后,运行符合预期:
生产者启动
消费者启动
并行加载 查找流数据
并行加载 查找流数据 进入等待状态
并行加载 响应保存
并行加载 保存完成 通知消费者
并行加载 查找流数据 被唤醒 等待时长:203
并行加载 查找流数据 返回桥接流
消费者 java.io.SequenceInputStream@4e8de630
...
消费者能在生产者首次通知时被准确唤醒,打断预读的场景也工作正常。
桥接流技术 (SequenceInputStream):这是实现中间态交接的关键。它允许将多个输入流(如已缓存的 ByteArrayInputStream 和未读完的 mNetworkStream)按顺序连接成一个逻辑上的连续流。WebView 可以无缝地先读取内存中的缓存数据,再自动切换至网络流读取剩余部分,实现了缓冲流与网络流的无缝桥接。
方案对比与总结
| 方案 |
优势 |
劣势 |
| 早期方案 |
实现简单,规避复杂场景 |
时间浪费(循环等待)、内存浪费(全量缓存)、资源利用率低 |
| 新方案 |
无循环等待、内存占用低(半缓冲)、支持中间态交接、资源利用率高 |
实现复杂度高,需精细处理线程同步和流合并 |
核心改进点:
- 公平锁替代非公平锁:使用
ReentrantLock(公平锁)+ Condition 替代 synchronized,确保消费者线程能及时被唤醒,避免饥饿。
- 桥接流技术:利用
SequenceInputStream 合并已缓冲流与网络流,支持“半成品”数据的无缝交接。
- 半缓冲机制:并行任务每次读取 4KB 后释放锁并通知,平衡了预读效率和响应及时性,避免了全量缓存的内存浪费。
写在最后
回顾方案演进,我们通过引入公平锁实现生产过程的及时打断,利用桥接流技术实现缓存与网络数据的无缝衔接。这套 H5优化 方案,相比最初的循环等待和全量缓存模式,显著提升了并行加载资源的利用率,更充分地挖掘了页面启动阶段的时间窗口潜力,是前端框架/工程化与 Native 端性能优化结合的典型案例。