使用动态路由优化H5加载速度

背景

目前的 APP 已经差不多有70%的业务使用 H5 来实现,但是一些页面的白屏现象严重。之前分别从原生层面对 WebView 加载 HTML 以及 H5 层面的 HTML 的加载过程进行了分析,确定出加载的耗时主要出现在渲染阶段,但是页面本身并不复杂,dom 的结构也很简单,应该渲染很快才对,所以又通过 chrome 的 timeline 来进行更详细分析的开发文档,结果如下图,可以确定出在一次加载过程中主要的耗时在 JS 的运算上面。

js 加载耗时

由此可以引发出一些思考,因为用来做分析的页面本身很简单,没有很多的 js 运算才对。

分析

经过一系列的监控发现加载过程的主要耗时在 js 的运算上面,目标页面并不复杂,仅仅是加载了一个列表而已,没有大量的业务需要进行 js 运算,那这些 js 都是哪来的?又查看了加载以后的 js 竟然有接近 2M,这显然是不正常的,原因出在哪?答案就在 react 项目的打包上,虽然我们的项目已经使用了多页面,但是实际上并不能叫多页面,更准确的应该叫多模块才对,一个模块的所有页面全部在一个总的路由下面,即在一个 html 中,同一模块下的所有页面使用一个 js,而即使通过路由加载一个很简单的页面也有把所有的这些 js 全部加载进来,这些势必造成了长时间的运算。

问题找到了,经过查阅资料发现比较好的解决办法就是使用动态路由。

现有路由

现在林林总总路由加起来有二十多个。经过打包以后的js大小有2M多。这就势必拖慢了加载的速度,在不考虑优化 js 逻辑相关的代码前,使用动态路由技术来对代码进行分离,做到按需加载应该能够提高加载速度。

1
2
3
4
5
6
7
8
9
<Route path="/" component={WaterIndex} />
<Route path="/AddWaterMeter" component={AddWaterMeter} />
<Route path="/ConfirmMeter" component={ConfirmMeter} />
...
<Route path="/FeedbackList" component={FeedbackList} />
<Route path="/PaymentHelp" component={PaymentHelp} />
</Router>

动态路由

原理就是将当前的代码在打包过程中分拆成多个小的包,在用户浏览过程中进行按需加载。示例代码

Webpack 配置

首先在 webpack.config.js 的 output 内加上 chunkFilename

1
2
3
4
5
6
7
output: {
path: path.join(__dirname, '/../dist/assets'),
filename: 'app.js',
publicPath: defaultSettings.publicPath,
// 添加 chunkFilename
chunkFilename: '[name].[chunkhash:5].chunk.js',
},

name 是在代码里为创建的 chunk 指定的名字,如果代码中没指定则 webpack 默认分配 id 作为 name。chunkhash 是文件的 hash 码,这里只使用前五位。

路由配置

之前的路由就像上面配置的一样,现在修改成如下的样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const AddWaterMeter = (location, callback) => {
require.ensure([], require => {
callback(null, require('./views/AddWaterMeter').default)
}, 'AddWaterMeter')
}
const WaterMeterList = (location, callback) => {
require.ensure([], require => {
callback(null, require('./views/WaterMeterList').default)
}, 'WaterMeterList')
}
const ConfirmMeter = (location, callback) => {
require.ensure([], require => {
callback(null, require('./views/ConfirmMeter').default)
}, 'ConfirmMeter')
}
ReactDOM.render(
<Router
history={hashHistory}
render={applyRouterMiddleware(useScroll())}
>
<Route path="/" Component={WaterIndex}>
<Route path="AddWaterMeter" Component={AddWaterMeter} />
<Route path="ConfirmMeter" getComponent={ConfirmMeter} />
<Route path="WaterMeterList" getComponent={WaterMeterList} />
...
<Route path="FeedbackList" getComponent={FeedbackList} />
<Route path="PaymentHelp" getComponent={PaymentHelp} />
</Route>
</Router>,
document.getElementById('app')
)

history 不变,将创建的路由传递进去。有几个属性的说明

  • path

    匹配路由,和之前的定义一样

  • getComponent

    对应于以前的 component 属性,但是这个方法是异步的,也就是当路由匹配时,才会调用这个方法。

    这里面有个 require.ensure 方法

    1
    require.ensure(dependencies, callback, chunkName)

    这是 webpack 提供的方法,这也是按需加载的核心方法。第一个参数是依赖,第二个是回调函数,第三个就是上面提到的 chunkName,用来指定这个 chunk file 的 name。

这里有可能会有一个异常:

The root route must render a single element

这是因为 module.exports 和 ES6 里的 export default 有区别。

如果是使用 es6 的写法,也就是你的组件都是通过 export default 导出的,那么在 getComponent 方法里面需要加入 .default。
如果是使用 CommonJS 的写法,也就是通过 module.exports 导出的,那就无须加 .default 了。

优化结果

经过上面的一通操作,再来看一下页面的加载速度,首先是可以明显的感知到速度变快。通过 timeline 来检测一下

优化结果

可以看到,速度提升了1s,对产品体验来说是一个很大的提升。

参考资料

React-Router动态路由设计最佳实践
react-router 按需加载