原文地址: https://engineering.innovid.com/code-splitting-using-lazy-loading-with-react-redux-typescript-and-webpack-4-3ec60140ec5a
做者: Aviv Shafir
摘要:Innovid網站使用Webpack4對一個React項目進行了優化改造。主要使用了新的optimization配置和動態注入功能。
Hey,這裏是Innovid,一個領先的視頻廣告平臺。咱們天天處理130萬小時的視頻,而在咱們的web項目中,常常會使用到Webpack。咱們很是喜歡這個工具。html
最近,咱們將一個項目遷移到了最新的Webpack4。它給咱們帶來了一些開箱即用的新特性,好比在構建時間上進行了很是大的優化。node
在本次遷移中,咱們決定使用懶加載這一Webpack最吸引人的特性來分割app中的主要代碼部分。react
代碼分割可以幫助你延遲加載用戶當前須要的內容,同時也能顯著地提高用戶體驗。儘管你沒有減小app的總代碼量,但你已經避免加載一些用戶也許永遠也用不到的代碼了。並且還可以在初始加載時減小加載的代碼數量。
—— React 文檔
Webpack根據你的應用程序構建了一個依賴關係圖。從你的入口文件開始,它遞歸遍歷全部文件和它們的依賴文件,使用loader和plugin對你的文件施了點魔法,最後就輸出了提供給用戶的生成包。webpack
咱們如今將生成包分爲app.js(咱們的應用代碼)和vendors.js(第三方庫)。
咱們使用webpack-bundle-analyzer插件來可視化兩個生成包: git
app.js大小116KB,vendors.js大小399KB
app.js是咱們程序的入口,因此自動打包成app.js。而第三方包vendors.js是使用了新的optimization
配置,將從node_modules
文件夾中引入的全部文件打包生成的。es6
mode: "production", entry: { app: path.join(__dirname, "index.tsx"), }, output: { path: path.resolve(__dirname, "public/dist"), publicPath: "", chunkFilename: "[name].js", filename: "[name].js" }, optimization: { runtimeChunk: { name: "manifest" }, splitChunks: { cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: "vendors", priority: -20, chunks: "all" } } } }
注意: 在Webpack4中,咱們再也不使用CommonChunkPlugin
了,它被splitChunks
和runtimeChunk
這兩個新API所取代。github
如今的vendors和app包都是用戶在第一次打開頁面室加載的。咱們發現能夠將一些「重量級」的組件懶加載來提高首屏體驗,而且減小初始包的體積。web
好比說:redux-form是一個管理react應用表單的庫,它只在一個名爲GenerateTags
的大型組件中使用。因爲它體積較大而且只在特定場景下被使用,因此用它來做爲懶加載的實驗對象是再好不過了。redux-form和GenerateTags組件能夠被抽取到單獨一個chunk中,這樣咱們在渲染首屏時請求的包體積更小。typescript
讓咱們看看如今流行的動態導入工具庫:react-loadable
。它基礎封裝了將來JS的新語法import()
。json
const GenerateTags = Loadable({ loader: () => import(/* webpackChunkName: "generateTags" */ "./GenerateTags"), loading: LoadingSpinner });
使用以後,咱們的包變成了下面這樣:
GenerateTags已經被抽取到單獨的一個chunk中,但redux-form仍然在vendor.js包裏。
結果不盡如人意,由於redux-form仍然在vendors.js包中,但咱們但願它跟GenerateTags都被抽取到一個不一樣的chunk中來實現按需加載。
之因此會出現這樣的狀況,是由於咱們在別的文件中也引用了redux-form。好比說咱們在combineReducers
中編寫了下面的代碼:
import { reducer as formReducer } from "redux-form"; const applicationReducer: Reducer<any> = combineReducers({ user, sidenav, navigation, //... form: formReducer });
這段代碼頂部的靜態導入語句致使redux-form庫成了咱們vendors包的一部分。也就是說,Webpack認爲它已經被靜態導入成咱們的app入口依賴樹的一部分,因此不能被懶加載。
爲了解決這個問題,咱們決定動態注入redux-form reducer。首先,咱們移除了導入redux-form reducer的語句,而且加了下面的代碼來實現動態注入redux reducer:
export function injectAsyncReducer(store, name, asyncReducer) { if (store.asyncReducers[name]) { return; } store.asyncReducers[name] = asyncReducer; store.replaceReducer(createReducer(store.asyncReducers)); } export const configureStore = (initialState: AppState) => { const enhancer = compose(applyMiddleware(...getMiddleware())); const store: any = createStore(createReducer(), initialState, enhancer); store.asyncReducers = {}; return store; }; const createReducer = (asyncReducers = {}) => { return combineReducers({ user, sidenav, navigation, //... ...asyncReducers }); };
最後,咱們在GenerateTags組件的componentDidMount中調用injectAsyncReducer方法。
public componentDidMount() { const reduxFormReducer = require("redux-form").reducer; injectAsyncReducer(store, "form", reduxFormReducer); }
注意,不推薦從組件直接獲取一個store的引用,由於這樣會致使你在作服務端渲染時出現一些問題。
在這裏你能夠閱讀更多注入異步代碼和使用HOC的知識。
咱們在項目中使用了typescript。咱們必須在tsconfig.json
中更新esnext的module配置,以及設置removeComments
爲false
(要支持動態注入,TS的版本必須高於2.4)。這樣,以前的動態注入纔會起做用。經過「告訴」typescript編譯器避開咱們的import語句,而且不要對它們進行轉碼來讓Webpack正常工做。
{ "compilerOptions": { "target": "es5", "sourceMap": false, "inlineSourceMap": true, "module": "esnext", "moduleResolution": "node", "jsx": "react", "preserveConstEnums": true, "removeComments": false, "lib": ["es6", "dom"] }, "types": ["node"] }
最後的結果就像下面這樣:
vendors.js 314 KiB, app.js 96.6 KiB, generateTags.js 23.2 KiB, vendors~generateTags.js 90.2 KiB
最後咱們成功了,GenerateTags和它的依賴文件redux-form被提取出vendor.js而且可以被按需加載。
咱們推薦你閱讀這個文章來優化Webpack。
查看更多我翻譯的Medium文章請訪問:
項目地址: https://github.com/WhiteYin/translation
SF專欄: https://segmentfault.com/blog/yin-translation