藉助Code Splitting 提高單頁面應用性能

近日的工做集中於一個單頁面應用(Single-page application),在項目中嘗試了聞名已久的Code splitting,收穫極大,特此分享。javascript

Why we need code splitting

SPA的客戶端路由極大的減小了Server 與 Client端之間的Round trip,在此基礎上,咱們還能夠藉助Server Side Rendering 砍掉客戶端的初次頁面渲染時間(這裏是SSR實現的參考連接:ReactAngular2).
仍然有一個問題廣泛存在着:隨着應用複雜度/規模的增長,應用初始所加載的文件大小也隨之增長。咱們能夠經過將文件分割成按需加載的chunks來解決這一問題,對於初始頁面,只請求他所用到的模塊的相關文件,等咱們進入新的路由,或者使用到一些複雜的功能模塊時,才加載與之相關的chunk。
藉助於webpackreact-router(目前個人應用是基於React開發的),咱們能夠快速實現這些按需加載的chunks。html

webpack

Webpack是很是火的一個module bundler,這裏是一個很好的入門參考連接。
咱們能夠藉助代碼中定義split point以建立按需加載的chunk。
使用require.ensure(dependencies, callback)能夠加載 CommonJs modules, 使用require(dependencies, callback)加載 AMD modules。webpack會在build過程當中檢測到這些split points,建立chunks。java

React router

React router 是一個基於React且很是流行的客戶端路由庫。
咱們能以plain JavaScript object或者declaratively的形式定義客戶端路由。
Plain JavaScript way:react

let myRoute = {
  path: `${some path}`,
  childRoutes: [
    RouteA,
    RouteB,
    RouteC,
  ]
}

declaratively way:webpack

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中(參考連接)。git

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纔會加載。
routesgithub

/* ---            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 renderweb

...
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 renderingreact-router

...
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.app

謝謝,但願能指正個人錯誤!

最後附一張目前項目的chunks圖:

相關文章
相關標籤/搜索