从根源缩小 webpack 打包的 React App 体积

用 webpack 打包 React App 可谓是 React 开发中的最佳实践,但是有个令人十分头疼的问题就是在堆上了一堆依赖之后,用 webpack 打包出来的东西体积非常非常非常大,加载和首屏渲染的时间就要非常非常非常久,用户体验自然也就非……很不好。有很多前辈已经研究了很多缩小 webpack 打包出的 React App 体积的办法,这里我想讲一下自己踩这个大坑的经历以及发现的一个可行的方案。

上一次更文已经是四……三……月份了?再不写点啥这里都有长草了……

在这之前

关于如何缩小 webpack 打包出的 bundle 的体积,已经有很多的引路人为我们填下了很多的坑,所以如果你想要缩减体积提高 React App 的首屏渲染效率,你可以参考以下的这些方法:

将第三方库与业务代码分离打包

大概是个治标不治本的方法,因为要正常渲染还是都要加载的,只是分开之后业务代码的部分体积会小一些,而且 React 和 ReactDOM 这类的库可以直接从 CDN 取得。这个地方目前据我所知有三种主流的方法:

一个是修改 webpack.config.js 中的 vendor 和使用 CommonsChunkPlugin 来分离 vendor 和 bundle,可以参考: - http://blog.csdn.net/tyro_java/article/details/54755610; - https://webpack.github.io/docs/code-splitting.html#multiple-entry-chunks

二是通过 webpack.config.js 中的 external 把模块的对象暴露到 window 中去,个人不是很推荐这种方法,虽然很方便,但不太好维护什么的;

三是用 webpack 的 DLL 方式来分离: - http://www.jianshu.com/p/a5b3c2284bb6。

去掉不必要的东西和使用 uglifyJS 压缩

诸如 react-hot-loader 和 redux-devtools 一类的东西生产环境是不需要的,所以在 dist 的时候就不要打包进去了。

以及很重要也很有效的一个方法就是用 uglifyJS 插件压缩 JS 代码,webpack 中自带了 UglifyJsPlugin,只需要修改一下 webpack config 中的 plugin 就可以了:

new webpack.optimize.UglifyJsPlugin({
  compress: {
    warnings: false,
    comments: false
  }
})

把代码分割成小块 (Code Split)

Code Split 的应用场景是当很多时候用户加载一个页面的时候只需要部分的内容,而却要下载所有的页面,因为它们被 webpack 一起打包在 bundle 当中了。解决方法就是把代码分块,然后按需加载。此方案支持 CommonJS 和 AMD, 但是暂时不支持 ES6 module. 这个方案可能对那些纠结 SPA 体积过大的开发者们很有帮助:

https://webpack.github.io/docs/code-splitting.html

从服务器端着手优化

从服务器端下手也是在正式发布生产环境之前一个很重要的环节——例如,开启 gzip 压缩的话能让 bundle 的体积缩小不少。以及不要忘记还有一个屡试不爽的方案——服务端渲染(SSR)。这两个方案的要求比较高也很好用,但是有些场景下并不适用例如你没有服务器的控制权

直接从 React 上下手

在我完成一个 React 项目的过程中也因为体积太大而捣鼓了好久,上面的这些方法也或多或少试过,鉴于应用场景的特殊性和广泛性有些上文提到的有效的方法都无法实践在这个项目中。一个小小的东西打包出来有 360K+ 的大小。考虑到其中 React + ReactDOM + redux 就占了将近一半(170K 左右)的空间,所以能否从 React 上下手来减少 bundle 的体积?

答案是肯定的,但是这并不是要你去自己精简 React —— 为了一个小项目去做这样的事情实在是太费神了,不如直接重构。好在,现在市面上也有一些 React 的精简版本,我们可以直接使用它们,有些轻量版的 React 甚至可以平滑过渡,实在是我这种懒人的福音

当前有很多对 React 进行抽象精简或者自行实现 Virtual DOM 和 JSX 的方案,例如 deku, react-lite, preact 等等。

react-lite

这里之所以不介绍 deku 是因为虽然 deku 有一些诸如 VDOM 的关键理念,但是如果你想从 React 转到 deku 相当于重构。

react-lite 是一个 react 的轻量实现。日常用到的 React API 几乎都可以在 react-lite 中跑起来。与原有的 react 相比 ,react-lite 保留了 react 的大量特性,例如 VDOM,JSX,等等;当然也丢了一些某些场景下用不到的东西,如果你不想要服务端渲染的话可以考虑使用 react-lite。

React-lite supports the core APIs of React, such as Virtual DOM, intended as a drop-in replacement for React, when you don’t need server-side rendering in browser(no ReactDOM.renderToString & ReactDOM.renderToStaticMarkup).

react-lite 也是我现在正在使用的代替 react 的方案,从某种意义上讲,react-lite 在某些方面(例如渲染等)的性能优于 react. 除了能有效地缩小 bundle 的体积(170K+ -> 30K)以及配置方便以外,它最吸引人的地方还在于:

它能和 react-router / redux 完美兼容!

它能和 react-router / redux 完美兼容!

它能和 react-router / redux 完美兼容!

简直太棒了有没有!!这意味着用 react-router + redux 构建的 react 项目几乎就可以直接平滑地过渡到 react-lite 上!!而且对于 react 的一些 unittest, react-lite 都能跑过。

从 react 过渡到 react-lite 也十分容易,在大部分情况下两者是可以兼容的,直接为 webpack 配置一个 alias,把对 react 的引用指向 react-lite 即可:

// webpack.config.js
{
    resolve: {
        alias: {
            'react': 'react-lite',
            'react-dom': 'react-lite'
        }
    }
}

https://github.com/Lucifier129/react-lite

preact

react-lite 能把 150K+ 的 react & react-dom 弄到 25K 左右,接下来介绍的 preact 号称是“3KB 的 react”,而且与 react 有相似的 API:

Fast 3kB alternative to React, with the same ES2015 API.

All the power of Virtual DOM components, without the overhead:

  • Familiar React API & patterns: ES6 Class and Functional Components
  • Extensive React compatibility via a simple preact-compat alias
  • Everything you need: JSX, VDOM, React DevTools, HMR, SSR..
  • A highly optimized diff algorithm and seamless Server Side Rendering
  • Transparent asynchronous rendering with a pluggable scheduler

和 react-lite 一样,它的渲染性能在某些场景下也能超过 react,毕竟没有了那么多奇奇怪怪的东西,性能飞起来也不足为奇嘛。

不过,体积小了 50 倍,相应地你也要付出一些代价。preact 不像 react-lite 那样可以平滑过渡,要修改组件的代码,自己配置 babel 对 JSX 的转译,很多东西也都需要替换成 preact 专用的东西,相对的讲不是那么方便,据说 react 的单元测试 preact 只能跑过一半。但是想到体积减小的大胜利,这点小小的工作就不值一提了。

https://github.com/developit/preact

后记

通过上面的方案以及直接对 React 动刀子,项目的体积直接从原来的 360K 左右变到 170K 上下了,首屏渲染的速度也改善了很多。后续如果还有什么行之有效的优化方法再补充这篇文章吧。