Tăng tốc Ứng dụng React Front-End của bạn bằng cách sử dụng Lazy Loading
() translation by (you can also view the original English article)
Một thách thức không ngừng đối với các nhà phát triển front-end là hiệu năng của các ứng dụng. Làm cách nào chúng ta có thể cung cấp một ứng dụng mạnh mẽ và đầy đủ tính năng cho người dùng của chúng ta mà không buộc họ phải chờ toàn bộ một trang được tải? Các kỹ thuật được sử dụng để tăng tốc một trang web rất nhiều đến nỗi nó thường có thể gây nhầm lẫn khi quyết định xem cần tập trung năng lượng của chúng ta ở đâu khi tối ưu hóa hiệu suất và tốc độ.
Rất may, giải pháp đôi khi không phức tạp như vẻ ngoài của nó. Trong bài viết này, tôi sẽ phân tích một trong những kỹ thuật hiệu quả nhất được sử dụng bởi các ứng dụng web lớn để tăng tốc trải nghiệm người dùng của họ. Tôi sẽ đi tìm hiểu một gói để giúp cho việc này và đảm bảo rằng chúng ta có thể cung cấp ứng dụng của mình cho người dùng nhanh hơn mà không nhận thấy có gì thay đổi.
Để một trang web nhanh hơn có nghĩa là gì?
Câu hỏi về hiệu suất web thì sâu rộng như chính nó vậy. Vì mục đích của bài viết này, tôi sẽ cố gắng định nghĩa hiệu suất theo các thuật ngữ đơn giản nhất: gửi càng ít càng tốt. Tất nhiên, đây có thể là một sự đơn giản hóa quá mức, nhưng thực tế mà nói, chúng ta có thể đạt được sự cải thiện tốc độ đáng kể bằng cách gửi ít dữ liệu hơn cho người dùng để tải xuống và gửi dữ liệu đó nhanh chóng.
Với mục đích của bài viết này, tôi sẽ tập trung vào phần đầu tiên của định nghĩa, gửi một lượng thông tin ít nhất có thể đến trình duyệt của người dùng.
Lúc nào cũng vậy, những tội phạm lớn nhất làm chậm các ứng dụng của chúng ta là hình ảnh và JavaScript. Trong bài viết này, tôi sẽ chỉ cho bạn cách xử lý vấn đề của các ứng dụng lớn và tăng tốc trang web của chúng ta trong quá trình này.
React Loadable
React Loadable là gói cho phép chúng ta lazy load JavaScript chỉ khi ứng dụng yêu cầu. Tất nhiên, không phải tất cả các trang web đều sử dụng React, nhưng để ngắn gọn, tôi sẽ tập trung vào việc cài đặt React Loadable trong một ứng dụng server được xây dựng với Webpack. Kết quả cuối cùng sẽ là nhiều tập tin JavaScript được gửi tự động đến trình duyệt của người dùng khi cần code đó.
Sử dụng định nghĩa của chúng ta từ trước đó, điều này đơn giản là chúng ta gửi ít hơn cho người dùng để dữ liệu có thể được tải xuống nhanh hơn và người dùng của chúng ta sẽ trải nghiệm một trang web hiệu quả hơn.
1. Thêm React Loadable
vào Component của bạn
Tôi sẽ lấy một ví dụ về React component, MyComponent
. Tôi sẽ giả sử component này được tạo thành từ hai tập tin, MyComponent/MyComponent.jsx
và MyComponent/index.js
.
Trong hai tập tin này, tôi định nghĩa React component chính xác như bình thường trong MyComponent.jsx
. Trong index.js
, tôi import react component và export nó, lần này gói trong hàm Loadable
. Sử dụng tính năng import
của ECMAScript, tôi có thể chỉ ra cho Webpack rằng tôi mong muốn tập tin này được tải động. Pattern này cho phép tôi dễ dàng tải bất kỳ component nào mà tôi đã viết. Nó cũng cho phép tôi phân tách logic giữa lazy-loanding và render. Nghe có vẻ phức tạp, nhưng thực tế nó trông như sau:
1 |
// MyComponent/MyComponent.jsx
|
2 |
|
3 |
export default () => ( |
4 |
<div> |
5 |
This component will be lazy-loaded! |
6 |
</div> |
7 |
)
|
1 |
// MyComponent/index.js
|
2 |
|
3 |
import Loadable from 'react-loadable' |
4 |
|
5 |
export default Loadable({ |
6 |
// The import below tells webpack to
|
7 |
// separate this code into another bundle
|
8 |
loader: import('./MyComponent') |
9 |
})
|
Sau đó tôi có thể import component của mình chính xác như bình thường:
1 |
// anotherComponent/index.js
|
2 |
|
3 |
import MyComponent from './MyComponent' |
4 |
|
5 |
export default () => <MyComponent /> |
Bây giờ thì tôi đã đưa React Loadable vào MyComponent
. Tôi có thể thêm logic vào component này sau nếu tôi muốn - điều này có thể bao gồm giới thiệu trạng thái tải hoặc trình xử lý lỗi cho component. Nhờ có Webpack, khi chúng ta chạy bản build của mình, giờ đây tôi sẽ được cung cấp hai gói JavaScript riêng biệt: app.min.js
là gói ứng dụng thông thường của chúng ta và myComponent.min.js
chứa code mà chúng ta vừa viết. Tôi sẽ thảo luận về cách cung cấp các gói này cho trình duyệt một lát sau.
2. Đơn giản hóa việc thiết lập với Babel
Thông thường, tôi phải bao gồm hai tùy chọn bổ sung khi truyền một đối tượng vào hàm Loadable
, modules
và webpack
. Những điều này trợ giúp Webpack xác định mô-đun nào chúng ta nên bao gồm. Rất may, chúng tôi có thể giảm bớt sự cần thiết phải bao gồm hai tùy chọn này với mọi component bằng cách sử dụng plugin react-loadable/babel
. Nó sẽ tự động bao gồm các tùy chọn cho chúng ta:
1 |
// input file
|
2 |
|
3 |
import Loadable from 'react-loadable' |
4 |
|
5 |
export default Loadable({ |
6 |
loader: () => import('./MyComponent') |
7 |
})
|
1 |
// output file
|
2 |
|
3 |
import Loadable from 'react-loadable' |
4 |
import path from 'path' |
5 |
|
6 |
export default Loadable({ |
7 |
loader: () => import('./MyComponent'), |
8 |
webpack: () => [require.resolveWeak('./MyComponent')], |
9 |
modules: [path.join(__dirname, './MyComponent')] |
10 |
})
|
Tôi có thể bao gồm plugin này bằng cách thêm nó vào danh sách các plugin trong tập tin .babelrc của mình, như sau:
1 |
{
|
2 |
"plugins": ["react-loadable/babel"] |
3 |
}
|
Bây giờ tôi đã tiến một bước gần hơn đến việc lazy-loading component của chúng ta. Tuy nhiên, trong trường hợp của tôi, tôi đang xử lý render bên phía server. Hiện tại, máy chủ sẽ không thể render các component lazy-loaded của chúng ta.
3. Render Component trên Server
Trong ứng dụng server của tôi, tôi có một cấu hình tiêu chuẩn trông giống như thế này:
1 |
// server/index.js
|
2 |
|
3 |
app.get('/', (req, res) => { |
4 |
const markup = ReactDOMServer.renderToString( |
5 |
<MyApp/> |
6 |
)
|
7 |
|
8 |
res.send(` |
9 |
<html>
|
10 |
<body>
|
11 |
<div id="root">${markup}</div> |
12 |
<script src="/build/app.min.js"></script>
|
13 |
</body>
|
14 |
</html>
|
15 |
`) |
16 |
})
|
17 |
|
18 |
app.listen(8080, () => { |
19 |
console.log('Running...') |
20 |
})
|
Bước đầu tiên là hướng dẫn cho React Loadable cái mà tôi muốn tất cả các mô-đun được tải sẵn. Điều này cho phép tôi quyết định những cái nào sẽ được tải ngay lập tức trên máy khách. Tôi làm điều này bằng cách sửa đổi tập tin server/index.js
của tôi như sau:
1 |
// server/index.js
|
2 |
|
3 |
Loadable.preloadAll().then(() => { |
4 |
app.listen(8080, () => { |
5 |
console.log('Running...') |
6 |
})
|
7 |
})
|
Bước tiếp theo sẽ là đẩy tất cả các component mà tôi muốn render thành một mảng để sau này chúng ta có thể xác định component nào yêu cầu tải ngay lập tức. Điều này là để HTML có thể được trả về với các gói JavaScript chính xác được bao gồm thông qua các thẻ script (sẽ nói thêm về điều này sau). Hiện tại, tôi sẽ sửa đổi tập tin server của tôi như sau:
1 |
// server/index.js
|
2 |
|
3 |
import Loadable from 'react-loadable' |
4 |
|
5 |
app.get('/', (req, res) => { |
6 |
const modules = [] |
7 |
const markup = ReactDOMServer.renderToString( |
8 |
<Loadable.Capture report={moduleName => modules.push(moduleName)}> |
9 |
<MyApp/> |
10 |
</Loadable> |
11 |
)
|
12 |
|
13 |
res.send(` |
14 |
<html>
|
15 |
<body>
|
16 |
<div id="root">${markup}</div> |
17 |
<script src="/build/app.min.js"></script>
|
18 |
</body>
|
19 |
</html>
|
20 |
`) |
21 |
})
|
22 |
|
23 |
Loadable.preloadAll().then(() => { |
24 |
app.listen(8080, () => { |
25 |
console.log('Running...') |
26 |
})
|
27 |
})
|
Mỗi khi một component được sử dụng mà yêu cầu React Loadable
, nó sẽ được thêm vào mảng modules
. Đây là một quy trình tự động được thực hiện bởi React Loadable
, vì vậy đây là tất cả những gì được yêu cầu từ phía chúng ta cho quy trình này.
Bây giờ chúng ta có một danh sách các mô-đun mà chúng ta biết sẽ cần phải được render ngay lập tức. Vấn đề chúng ta gặp phải bây giờ là ánh xạ các mô-đun này sang các gói mà Webpack đã tự động tạo ra cho chúng ta.
4. Ánh xạ gói Webpack thành các mô-đun
Như vậy, bây giờ tôi đã hướng dẫn cho Webpack tạo myComponent.min.js và tôi biết rằng MyComponent
đang được sử dụng ngay lập tức, vì vậy tôi cần tải gói này trong payload HTML ban đầu mà chúng ta cung cấp cho người dùng. Rất may, React Loadable cung cấp một cách để chúng ta đạt được điều này. Trong tập tin cấu hình Webpack cho client của tôi, tôi cần bao gồm một plugin mới:
1 |
// webpack.client.config.js
|
2 |
|
3 |
import { ReactLoadablePlugin } from 'react-loadable/webpack' |
4 |
|
5 |
plugins: [ |
6 |
new ReactLoadablePlugin({ |
7 |
filename: './build/loadable-manifest.json' |
8 |
})
|
9 |
]
|
Tập tin loadable-manifest.json sẽ cung cấp cho tôi ánh xạ giữa các mô-đun và gói để tôi có thể sử dụng mảng modules
mà tôi thiết lập trước đó để tải các gói tôi biết là tôi cần. Trong trường hợp của tôi, tập tin này có thể trông giống như thế này:
1 |
// build/loadable-manifest.json
|
2 |
|
3 |
{
|
4 |
"MyComponent": "/build/myComponent.min.js" |
5 |
}
|
Điều này cũng sẽ yêu cầu một tập tin manifest Webpack thông thường để bao gồm ánh xạ giữa các mô-đun và tập tin cho các mục đích Webpack nội bộ. Tôi có thể làm điều này bằng cách bao gồm một plugin Webpack khác:
1 |
plugins: [ |
2 |
new webpack.optimize.CommonsChunkPlugin({ |
3 |
name: 'manifest', |
4 |
minChunks: Infinity |
5 |
})
|
6 |
]
|
5. Bao gồm các gói trong HTML của bạn
Bước cuối cùng trong việc tải các gói động của chúng ta trên server là đưa các gói này vào HTML mà chúng ta cung cấp cho người dùng. Đối với bước này, tôi sẽ kết hợp đầu ra của bước 3 và 4. Tôi có thể bắt đầu bằng cách sửa đổi tập tin máy chủ mà tôi đã tạo ở trên:
1 |
// server/index.js
|
2 |
|
3 |
import Loadable from 'react-loadable' |
4 |
import { getBundles } from 'react-loadable/webpack' |
5 |
import manifest from './build/loadable-manifest.json' |
6 |
|
7 |
app.get('/', (req, res) => { |
8 |
const modules = [] |
9 |
const markup = ReactDOMServer.renderToString( |
10 |
<Loadable.Capture report={moduleName => modules.push(moduleName)}> |
11 |
<MyApp/> |
12 |
</Loadable> |
13 |
)
|
14 |
|
15 |
const bundles = getBundles(manifest, modules) |
16 |
|
17 |
// My rendering logic below ...
|
18 |
})
|
19 |
|
20 |
Loadable.preloadAll().then(() => { |
21 |
app.listen(8080, () => { |
22 |
console.log('Running...') |
23 |
})
|
24 |
})
|
Trong tập tin này, tôi đã import tập tin manifest và yêu cầu React Loadable tạo một mảng với ánh xạ mô-đun/gói. Điều duy nhất còn lại để tôi làm là render các gói này thành một chuỗi HTML:
1 |
// server/index.js
|
2 |
|
3 |
app.get('/', (req, res) => { |
4 |
// My App & modules logic
|
5 |
|
6 |
res.send(` |
7 |
<html>
|
8 |
<body>
|
9 |
<div id="root">${markup}</div> |
10 |
<script src="/build/manifest.min.js"></script>
|
11 |
${bundles.map(({ file }) => |
12 |
`<script src="/build/${file}"></script>` |
13 |
}).join('\n')} |
14 |
<script src="/build/app.min.js"></script>
|
15 |
</body>
|
16 |
</html>
|
17 |
`) |
18 |
})
|
19 |
|
20 |
Loadable.preloadAll().then(() => { |
21 |
app.listen(8080, () => { |
22 |
console.log('Running...') |
23 |
})
|
24 |
})
|
6. Tải Gói kết xuất trên Server vào Client
Bước cuối cùng để sử dụng các gói mà chúng ta đã tải trên server là sử dụng chúng trên client. Làm điều này rất đơn giản. Tôi chỉ cần hướng dẫn React Loadable
tải trước bất kỳ mô-đun nào mà nó tìm thấy có sẵn ngay lập tức:
1 |
// client/index.js
|
2 |
|
3 |
import React from 'react' |
4 |
import { hydrate } from 'react-dom' |
5 |
import Loadable from 'react-loadable' |
6 |
|
7 |
import MyApplication from './MyApplication' |
8 |
|
9 |
Loadable.preloadReady().then(() => { |
10 |
hydrate( |
11 |
<MyApplication />, |
12 |
document.getElementById('root') |
13 |
);
|
14 |
});
|
Phần kết luận
Theo quy trình này, tôi có thể chia gói ứng dụng của mình thành nhiều gói nhỏ hơn khi tôi cần. Bằng cách này, ứng dụng của tôi sẽ gửi ít hơn cho người dùng và chỉ khi họ cần. Tôi đã giảm số lượng code cần gửi để có thể gửi nhanh hơn. Điều này có thể làm cho hiệu suất tăng đáng kể cho các ứng dụng lớn hơn. Nó cũng có thể thiết lập các ứng dụng nhỏ hơn để tăng trưởng nhanh chóng khi cần.