借助Code Splitting 提升单页面应用性能
近日的工作集中于一个单页面应用(Single-page application),在项目中尝试了闻名已久的Code splitting,收获极大,特此分享。
Why we need code splitting
SPA的客户端路由极大的减少了Server 与 Client端之间的Round trip,在此基础上,我们还可以借助Server Side Rendering 砍掉客户端的初次页面渲染时间(这里是SSR实现的参考链接:React,Angular2).
仍然有一个问题普遍存在着:随着应用复杂度/规模的增加,应用初始所加载的文件大小也随之增加。我们可以通过将文件分割成按需加载的chunks来解决这一问题,对于初始页面,只请求他所用到的模块的相关文件,等我们进入新的路由,或者使用到一些复杂的功能模块时,才加载与之相关的chunk。
借助于webpack与react-router(目前我的应用是基于React开发的),我们可以快速实现这些按需加载的chunks。
webpack
Webpack是非常火的一个module bundler,这里是一个很好的入门参考链接。
我们可以借助代码中定义split point
以创建按需加载的chunk。
使用require.ensure(dependencies, callback)
可以加载 CommonJs modules, 使用require(dependencies, callback)
加载 AMD modules。webpack会在build过程中检测到这些split points,创建chunks。
React router
React router 是一个基于React且非常流行的客户端路由库。
我们能以plain JavaScript object或者declaratively的形式定义客户端路由。
Plain JavaScript way:
let myRoute = {
path: `${some path}`,
childRoutes: [
RouteA,
RouteB,
RouteC,
]
}
declaratively way:
const routes = (
<Route component={Component}>
<Route path="pathA" component={ComponentA}/>
<Route path="pathB" component={ComponentB}/>
</Route>
)
React router 可以实现代码的lazy load, 而我们正好可以把split points 定义在这些lazy load code中(参考链接)。
Code Splitting implement
below is a demo of create two on demand loaded chunks, chunk A will load once when enter rootUrl/A, chunk B will load once when enter rootUrl/B.
接下来的代码就是创建按需加载的chunks的例子,chunk A 只有当进入rootUrl/A才会加载,chunk B 只有当进入rootUrl/B才会加载。
routes
/* --- RootRoute --- */
...
import RouteA from './RouteA'
import RouteB from './RouteB'
export default {
path: '/',
component: App,
childRoutes: [
RouteA,
RouteB,
],
indexRoute: {
component: Index
}
}
/* --- RouteA --- */
...
export default {
path: 'A',
getComponent(location, cb) {
require.ensure([], (require) => {
cb(null, require(`${PathOfRelatedComponent}`))
}, 'chunkA')
}
}
/* --- RouteB --- */
...
export default {
path: 'B',
getComponent(location, cb) {
require.ensure([], (require) => {
cb(null, require(`${PathOfRelatedComponent}`))
}, 'chunkB')
}
}
client side code for client side render
...
import { match, Router } from 'react-router'
const { pathname, search, hash } = window.location
const location = `${pathname}${search}${hash}`
//use match to trigger the split code to load before rendering.
match({ routes, location }, () => {
render(
<Router routes={routes} history={createHistory()} />,
document.getElementById('app')
)
})
server code for server side rendering
...
app.createServer((req, res) => {
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
if (error)
writeError('ERROR!', res)
else if (redirectLocation)
redirect(redirectLocation, res)
else if (renderProps)
renderApp(renderProps, res)
else
writeNotFound(res)
}).listen(PORT)
function renderApp(props, res) {
const markup = renderToString(<RoutingContext {...props}/>)
const html = createPage(markup)
write(html, 'text/html', res)
}
export function createPage(html) {
return `
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>My Universal App</title>
</head>
<body>
<div id="app">${html}</div>
<script src="/__build__/main.js"></script>
</body>
</html>
`
}
实现中可能会遇到的坑
取决于你是如何写自己的模块的,你可能会遇到这个错误:React.createElement: type should not be null, undefined, boolean, or number. It should be a string (for DOM elements) or a ReactClass (for composite components). Check the render method of RoutingContext.
在require()
之后加一个.default
即可。
如果你收到了这样的错误提示:require.ensure is not function
, 增加一个polyfill即可: if (typeof require.ensure !== 'function') require.ensure = (d, c) => c(require)
,在Server端使用require来代替require.ensure.
谢谢,希望能指正我的错误!
最后附一张目前项目的chunks图: