你是在哪个地方去请求的?
对于一个纯前端页面来说,通过ajax获取页面数据是很常见的操作。对于React来说网上曾有过很多关于在componentWillMount
和componentDidMount
哪个里请求好。官方的推荐是后者:
componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。
不过这里我们讨论的是哪里请求最"快"。虽然这个问题不是本文讨论的目的,但我的看法是在componentWillMount
(已废弃)或constructor
里请求是快于componentDidMount
的,理由就是这两个周期在mount前面,至于两个周期相差的时间是多少取决于应用的复杂度,对于vue也是一样的道理。
所以我的论点是早发早请求早拿到接口数据。这时候会有小朋友问,你怎么知道越早调用发请求的方法,请求就真的会早点发出去呢,万一是等js主线程执行完后再去发。那我们就打开Chrome的开发者工具看看吧。
为了让效果更加明显我们主动地增加js的执行时间,来让差异更大以忽略运行环境影响。我们定义一个巨耗时的斐波那契方法:
function fibonacci(n){
if(n==0) return 0
else if(n==1) return 1
else return fibonacci(n-1) + fibonacci(n-2)
}
然后做个对照实验,把请求分别放在这里方法前面或者后面来看:
// A组
getApiData();
fibonacci(41); // 感觉再往上计算就会卡了,你们的电脑能算到多少?
// B组
fibonacci(41);
getApiData();
A组:
可以看到这里开始请求的时间是605ms的时候,对于这里所有指标的含义可以在这里看到 -> google网站
B组:
可以看到我的电脑跑了近2秒的斐波那契。从结果来看请求的时间也确实被延迟了,605ms -> 2.43s。故越早发请求,越早请求服务器。
我在圈外发请求
那么回到我们的问题。既然越早越好的话,componentWillMount
快于componentDidMount
,那么还能不能再提前一些?比如放到React组件外面?
import React from 'react';
getApiData() // 这里
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
理论上会快一点,但是一般不会太明显。那我们再往上看import React from 'react'
,要知道我们的请求方法是在你引入的各种库和框架之后运行的,其也会消耗大量的时间。所以我们干脆点直接把过程再往前提。这时候有小朋友问:那放哪呢?这里我们可以把请求数据的方法单独打一个包出来,比如就叫network.ts
,然后在webpack里新增这个入口最后会得到不同的js文件。
entry: {
main: [path.resolve(__dirname, `./src/index.tsx`)],
network: [path.resolve(__dirname, `./src/network.ts`)]
}
然后把网络请求的js放到应用程序的前面,这样甚至不用等我们应用的js加载完就可以发出请求了。
<script
src="<%= htmlWebpackPlugin.files.chunks.main && htmlWebpackPlugin.files.chunks.network.entry %>"
type="text/javascript"
></script>
<script
src="<%= htmlWebpackPlugin.files.chunks.main && htmlWebpackPlugin.files.chunks.main.entry %>"
type="text/javascript"
></script>
当然也许network.js
也会用到一些库辅助请求的过程,也许在主应用里也会用到,是不是就重复打包了呢?这时候可以用webpack再打一个公共包出来。相关配置可以看这里 -> splitChunks
optimization: {
minimizer: [
new TerserPlugin(),
new OptimizeCssAssetsPlugin({}),
],
splitChunks: {
cacheGroups: {
commons: {
name: 'vendors',
chunks: 'all',
minChunks: 2,
},
},
},
}
然后把公共的vendors
放最上面就配置好了。这里可能有小朋友有很多问题:你在另一个包里获得的数据怎么传给另外一个?这里可以简单地使用window
全局对象来保存。你可能还会问:那我组件加载完成了数据还没返回怎么办?这也好解决,只要我们存到window
的是Promise对象而不是数据本身就可以了。
function getApiData(): Promise<any> // 自定义一个返回Promise对象的请求函数
// network.ts
window.__request = getApiData();
// app.tsx
function getRequest() {
if (window.__request) {
const request = window.__request;
window.__request = undefined;
return request;
}
return getApiData();
}
getRequest().then(xxx)
这样就大功告成了,具体优化的数据怎么样呢?
第二列是安卓上8分位的用户(中位数的8位版本)的首屏出现耗时。可以看到最终提高了400毫秒的速度。观察仔细的同学可以发现第一列中load事件触发被拉后了大概300ms,这是为什么呢?这里推测是我的ajax请求block了load事件的触发,最开始首屏减去load时间是700ms,我们假设这就是请求的时间,首屏时间优化了400ms,优化的时间刚好是js和请求并行执行的时间,700 - 400 = 300。但是我没有证据,因为W3C规范只说js、css资源会影响并没有提到我的JSON数据请求。那我只好自己模拟一个很耗时的请求了。但是我在Chrome和手机实验的结果是不影响。难道问题出在机型或场景上?有想法的同学可以在下方评论打出你的看法,有新的调查我也会补充在评论。
如果你还想再提前请求的时间你可以选择使用客户端预请求,这需要和app的配合在打开网页前提前获取你的数据。如果能连webview一起预加载的当然更好。这里推荐一篇百度APP-Android H5首屏优化实践
其他优化
对于前端页面如果在app内打开的话,可以采用离线包的形式提前将html、js等下载至app,就无需加载获取。视觉上我们可以通过在html上放置loading图或骨架屏(下图)来提前第一帧画面的时间和用户体验。这里强烈推荐使用骨架屏,从设计角度来说可以帮助用户提前分析界面结构,为到来的数据做准备,还会给用户页面已经成型马上就要出来了的错觉。其他优化手段当然还有很多这里推荐一篇H5 秒开方案大全