前端在哪里发请求最快?mount周期里获取数据?react,vue网络请求优化

1,905 阅读5分钟

你是在哪个地方去请求的?

对于一个纯前端页面来说,通过ajax获取页面数据是很常见的操作。对于React来说网上曾有过很多关于在componentWillMountcomponentDidMount哪个里请求好。官方的推荐是后者:

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 秒开方案大全