Tự học ReactJS: React Performance – Code splitting

Tự học ReactJS: React Performance – Code splitting

Chia nhỏ code (code splitting) là một kỹ thuật cho phép chúng ta tải ít code hơn cho một website và tất nhiên nó sẽ làm app nhanh hơn. Ví dụ như chúng ta đang cài đặt một dashboard có xài thư viện D3 để vẽ chart. Người dùng sau đó sử dụng bắt đầu than phiền rằng màn hình login đang load quá chậm.

Giả sử, vấn đề ở đây là do bạn tải quá nhiều code lên một trang web. Bạn có thể đang tải cả code cho chart trong màn hình login và do thư viện D3 quá nặng nên nó cũng ảnh hưởng đến performance của trang login. Cho nên chúng ta cần loại bỏ thư viện D3 ra khỏi trang login và chỉ load D3 mỗi khi người dùng vào trang dashboard. Kỹ thuật này được gọi là lazy-loading hay on-demand loading.

May mắn rằng chúng ta có sẵn một cách trong JS để import các thư viện JS tự động là:

import('/some-module.js').then(
  module => {
    //  làm gì đó với những method từ module
  },
  error => {
    // error với module...
  },
)

Trong React, bạn có thể sử dụng cú pháp tương tự để lazy-loading component và bạn phải sử dụng React.Suspense như sau:

// smiley-face.js
import * as React from 'react'

function SmileyFace() {
  return <div>😃</div>
}

export default SmileyFace

// app.js
import * as React from 'react'

const SmileyFace = React.lazy(() => import('./smiley-face'))

function App() {
  return (
    <div>
      <React.Suspense fallback={<div>loading...</div>}>
        <SmileyFace />
      </React.Suspense>
    </div>
  )
}

Một trong những công cụ hữu ích để bạn có thể xác định được những phần code dư thừa trong JS là sử dụng tab Coverage trong Chrome Devtool.

Lazy load Globe component

Ví dụ chúng ta có một demo như sau. Có một checkbox sau khi click vào nó thì sẽ hiển thị hình ảnh về Globe:

import * as React from 'react'
// 💣 xoá import này
import Globe from '../globe'

// 🐨  sử dụng React.lazy để tạo Globe component sử dụng a dynamic import
// to get the Globe component from the '../globe' module.

function App() {
  const [showGlobe, setShowGlobe] = React.useState(false)

  // 🐨 bọc component bên dưới với <React.Suspense />
  // và a fallback.
  return (
    <div
      style={{
        display: 'flex',
        alignItems: 'center',
        flexDirection: 'column',
        justifyContent: 'center',
        height: '100%',
        padding: '2rem',
      }}
    >
      <label style={{marginBottom: '1rem'}}>
        <input
          type="checkbox"
          checked={showGlobe}
          onChange={e => setShowGlobe(e.target.checked)}
        />
        {' show globe'}
      </label>
      <div style={{width: 400, height: 400}}>
        {showGlobe ? <Globe /> : null}
      </div>
    </div>
  )
}

export default App

Sau đây là cách sử dụng để lazy loading component Globe mỗi khi người dùng chọn show globe trên màn hình:

Đáp án:

import * as React from 'react'

const Globe = React.lazy(() => import('../globe'))// gia sử chúng ta có component Globe

function App() {
  const [showGlobe, setShowGlobe] = React.useState(false)

  return (
    <div
      style={{
        display: 'flex',
        alignItems: 'center',
        flexDirection: 'column',
        justifyContent: 'center',
        height: '100%',
        padding: '2rem',
      }}
    >
      <label style={{marginBottom: '1rem'}}>
        <input
          type="checkbox"
          checked={showGlobe}
          onChange={e => setShowGlobe(e.target.checked)}
        />
        {' show globe'}
      </label>
      <div style={{width: 400, height: 400}}>
        <React.Suspense fallback={<div>loading globe...</div>}>
          {showGlobe ? <Globe /> : null}
        </React.Suspense>
      </div>
    </div>
  )
}

export default App

Load trước Globe component (eager loading)

Có một cách load <Globe /> nhanh hơn và chủ động hơn là khi người dùng hover chuột lên checkbox thì chúng ta sẽ bắt đầu load <Globe /> component lên. Với cách làm này, chúng ta sẽ không phải chờ đợi người dùng click vào checkbox mà chúng ta chủ động dựa vào hành động hover chuột của người dùng. Kỹ thuật này gọi là eager loading.

Đáp án:

import * as React from 'react'

const loadGlobe = () => import('../globe')
const Globe = React.lazy(loadGlobe)

function App() {
  const [showGlobe, setShowGlobe] = React.useState(false)

  return (
    <div
      style={{
        display: 'flex',
        alignItems: 'center',
        flexDirection: 'column',
        justifyContent: 'center',
        height: '100%',
        padding: '2rem',
      }}
    >
      <label
        style={{marginBottom: '1rem'}}
        onMouseEnter={loadGlobe}
        onFocus={loadGlobe}
      >
        <input
          type="checkbox"
          checked={showGlobe}
          onChange={e => setShowGlobe(e.target.checked)}
        />
        {' show globe'}
      </label>
      <div style={{width: 400, height: 400}}>
        <React.Suspense fallback={<div>loading globe...</div>}>
          {showGlobe ? <Globe /> : null}
        </React.Suspense>
      </div>
    </div>
  )
}

export default App

Chúng ta sẽ gọi loadGlobe cho các event onMouseEnter={loadGlobe} onFocus={loadGlobe}.

Prefetch Globe với webpack config

Có một comment rất hay trong webpack mà bạn có thể sử dụng để prefetch module:

import(/* webpackPrefetch: true */ './some-module.js')

Sau đó, webpack build và add script vào head của document như sau:

<link rel="prefetch" as="script" href="/static/js/1.chunk.js">

Với prefetch, trình duyệt sẽ tự động tải lên script vào cache và sẽ được gọi ra khi cần thiết. Điều này làm cho tương tác của người dùng nhanh hơn và tải script lên từ cache nhanh hơn nhiều khi request script từ server.

Bạn có thể xem demo tại đây

Bạn sẽ thấy 2 scripts của <Globe /> component được thêm vào head với rel="prefetch" như sau:

Đáp án:

import * as React from 'react'

const Globe = React.lazy(() => import(/* webpackPrefetch: true */ '../globe'))

function App() {
  const [showGlobe, setShowGlobe] = React.useState(false)

  return (
    <div
      style={{
        display: 'flex',
        alignItems: 'center',
        flexDirection: 'column',
        justifyContent: 'center',
        height: '100%',
        padding: '2rem',
      }}
    >
      <label style={{marginBottom: '1rem'}}>
        <input
          type="checkbox"
          checked={showGlobe}
          onChange={e => setShowGlobe(e.target.checked)}
        />
        {' show globe'}
      </label>
      <div style={{width: 400, height: 400}}>
        <React.Suspense fallback={<div>loading globe...</div>}>
          {showGlobe ? <Globe /> : null}
        </React.Suspense>
      </div>
    </div>
  )
}

export default App

thaunguyen.com via Epic React by Kent C.Dodds

Leave a Comment