原文地址: 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