借助Code Splitting 提升單頁面應用性能
來自: http://www.cnblogs.com/E-WALKER/p/5166623.html
近日的工作集中于一個單頁面應用(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圖:
