首屏关键接口是影响首屏内容展示的关键因素,以react框架为例,大多数首屏请求都是放在useEffect内来发起的,从浏览器的性能检测工具来看,需要等待框架的静态资源加载完毕之后请求才能发起。这样可能导致白屏时间或者骨架屏过渡时间过程,影响实际内容的展示。本文将分享4种预请求的方案,供大家参考。
客户端预请求
webview初始化数据
如果h5页面是在客户端中,客户端打开一个h5是依托于webview的,webview的初始化是需要一定时间的。下图是美团APP做的数据调研:
可以看出APP冷启动打开webview耗时的时间相对而言是很高的。那在这段时间中,我们可以并行去请求接口。
方案介绍
- 客户端在初始化webview的同时,异步获取接口数据
scala
// 客户端在初始化webview的同时,异步获取接口数据
public class WebViewTest extends AppCompatActivity {
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 初始化 WebView
webView = findViewById(R.id.webview);
// 假设预请求react的github信息,请求完该接口时会缓存在内存中
PreloadManager.getInstance().preloadUrl("https://api.github.com/repos/facebook/react");
// 简单实现下jsbridge,addJavascriptInterface可以把java对象注入到webview里,让webview调用java方法
webView.addJavascriptInterface(new WebAppInterface(this, webView), "nativeObject");
// 加载网页
webView.loadUrl("http://10.0.2.2:3001");
}
}
- h5通过jsbridge获取客户端缓存
javascript
useAsyncEffect(() => {
// jsbridge预请求
const result = await window.BridgeNameSpace.preFetch({
url: "https://api.github.com/repos/facebook/react",
});
window.BridgeNameSpace.showToast({ title: JSON.stringify(result) });
}, [])
流程可能变成这个样子,减少了一步请求的时间
如果页面不是在自己家客户端内打开,无法借助客户端能力,例如微信分享,外投广告这种场景,有别的方案吗?
ssr服务端渲染预请求
SSR
将组件在服务端直接渲染成 HTML 字符串,作为服务端响应返回给浏览器,最后在浏览器端将静态的 HTML"激活"(hydrate) 为能够交互的客户端应用。
可能带来更快的首屏加载
服务端渲染的 HTML 无需等到所有的 JavaScript 都下载并执行完成之后才显示,所以你的用户将会更快地看到完整渲染的页面。除此之外,数据获取过程在首次访问时在服务端完成,相比于从客户端获取,可能有更快的数据库连接 。这通常可以带来更高的核心 Web 指标、更好的用户体验,而对于那些"首屏加载速度与转化率直接相关"的应用来说,这点可能至关重要。
方案介绍
- 服务端node代码:express结合react的api,收集挂载在App组件上的请求方法,然后在服务端请求,并注入到window上
ini
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App.jsx';
const app = express();
app.use(express.static('public'));
app.get('*', async (req, res) => {
// 1. 检查是否存在静态数据请求方法
let pageData = {};
if (App.fetchData) {
// 调用静态方法获取数据
pageData = await App.fetchData();
}
// 2. 渲染组件为字符串
const reactHtml = renderToString(<App data={pageData} />);
// 3. 将数据注入到window对象
const injectedData = `
<script>
window.__INITIAL_DATA__ = ${JSON.stringify(pageData)};
</script>
`;
const html = `
<!DOCTYPE html>
<html>
<head>
<title>SSR Data Injection</title>
</head>
<body>
<div id="root">${reactHtml}</div>
${injectedData}
<script src="/client.js"></script>
</body>
</html>
`;
res.send(html);
});
app.listen(3000);
- 客户端取服务端注入在window上的接口数据渲染界面
javascript
// ------------------ 客户端代码 client.js ------------------
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import App from './App.jsx';
// 从window获取服务端注入的数据
const initialData = window.__INITIAL_DATA__ || {};
hydrateRoot(
document.getElementById('root'),
<App data={initialData} />
);
// ------------------ 组件 App.jsx ------------------
export default function App({ data }) {
return (
<div>
<h1>User Data:</h1>
<p>Name: {data.name || 'Loading...'}</p>
<p>Age: {data.age || ''}</p>
</div>
);
}
// 静态数据请求方法(服务端会调用)
App.fetchData = async () => {
// 模拟API请求
return new Promise(resolve => {
setTimeout(() => {
resolve({
name: 'Alice',
age: 28
});
}, 100);
});
};
流程可能变成这个样子
那没有ssr,只有csr,有别的方案吗?
边缘函数(ER)预请求
CDN
内容分发网络(Content Delivery Network,CDN)是建立并覆盖在承载网上,由不同区域的服务器组成的分布式网络。将源站资源缓存到全国各地的边缘服务器,供用户就近获取,降低源站压力。
CDN将源站资源缓存到阿里云遍布全球的加速节点,当终端用户请求访问和获取源站资源时无需回源,可就近获取CDN节点上已经缓存的资源,提高资源访问速度,同时分担源站压力。目前CDN部分节点已支持通过IPv6访问。
边缘计算
基于遍布全球的节点,DCDN提供了智能弹性的计算和存储服务,即边缘函数和边缘存储。您可以将在线服务或轻量应用直接部署至全球边缘节点,就近处理客户端的请求,以获得更低的延迟。同时,您无需再运维服务器资源,Serverless将自动为您分配足够的计算和存储资源。
边缘函数(EdgeRoutine,简称ER)是一项基于Serverless架构的服务,它允许开发者编写JavaScript代码并在阿里云全球边缘节点上部署和执行,支持ES6语法和标准的Web Service Worker API。通过这种技术,用户的请求可以直接在离用户最近的边缘节点上得到响应处理,从而显著减少延迟、提高响应速度,并实现更低时延的计算体验。
方案介绍
- html接入时按照约定自定义一个html标签,来标识需要er来处理,attr来代表参数,例如:
ini
<x-er type="prefetch" url="https://api.github.com/repos/facebook/react" />
- er程序,HTMLStream流式传输
javascript
addEventListener('fetch', (event) => {
event.respondWith(handle(event));
});
async function handle(event) {
// 1.假设exmaple页面返回的是待修改的HTML页面。
const response = await fetch("http://www.example.com");
let { readable, writable } = new TransformStream();
const htmlStream = new HTMLStream(
response.body, // 放入需要改写的HTML流
[[
"x-er", // 元素选择器,表示选择所有的`x-er`标签。
{
// 注册回调函数对象,名为element的回调函数会在x-er标签(在DOM中为ElementNode)被调用。
// 调用时可以传入object来更改e的信息。
element: function(e) {
// 更改属性href
const url = e.getAttribute("url");
fetch(url).then((result) => {
// 2. 通过流式传输结果
})
}
}
]]);
// 3.返回修改后的请求到浏览器。HTMLStream是个ReadableStream,所以任何能使用
// ReadableStream的地方均可使用HTMLStream。
return new Response(htmlStream);
}
修改后的流程如下:
没有cdn,有别的方案吗?
文档解析head预请求
方案介绍
javascript
// head里的script
<script>
fetch("https://api.github.com/repos/facebook/react")
.then((res) => res.json())
.then((data) => {
window["head-request-facebook/react"] = data;
window.dispatchEvent(
new CustomEvent("head-request", {
detail: {
url: "https://api.github.com/repos/facebook/react",
data,
},
})
);
});
</script>
// 请求
function headPrefetch(url) {
return new Promise((resolve) => {
const listener = (e) => {
const customEvent = e;
if (customEvent.detail?.url === url) {
resolve(customEvent.detail?.data);
}
};
window.addEventListener("head-request", listener, {
once: true,
});
const customWindow = window;
if (customWindow[url]) {
resolve(customWindow[url]);
}
});
}
修改后的流程如下:
总结
上述方案只是简单示例,每种方案都需要再仔细思考下细节和边界条件,比如:客户端预请求如果失败了怎么办?缓存什么时候清空?ssr如果首屏接口耗时长,白屏时间会不会很长?边缘请求登录态怎么带?每个案例可以细化成一个解决方案,抛转引玉,欢迎大家提出更多的方案