2013 年 JSConf 大会上 Facebook 宣布 React 开源,其突破性的创新理念,声明式的代码风格,基于组件嵌套编码理念以及跨平台等优点,获得了越来越多前端工程师的热爱。同时将前端代码工程化提高到一个新的高度。
众所周知,React 的核心理念是模块的组合,但是如果首屏依赖模块过多,或者使用到一些大型模块等,将会显著拖累首屏渲染速度,影响用户体验。
我们尝试通过首次加载模块时仅渲染部分内容,然后在其他模块延迟加载完毕后再渲染剩余部分的方式,提高首屏加载(渲染)速度。
本文将分享一些关于模块延迟加载(懒加载)实现的探索和经验(Reactjs、React-Native 均适用,本文以 Reactjs 示例)。
比如现在我们有一个模块 Hello,demo 代码如下:
class Hello extends Component {
constructor(props){
super(props)
this.state = {
}
}
render() {
return (
<div className="container">
<div>{this.props.title}</div>
</div>
);
}
}
核心思路:懒加载的核心就是异步加载。可以先展现给用户一个简单内容,或者干脆空白内容。同时在后台异步加载模块,当模块异步加载完毕后,再重新渲染真正的模块。
我们以上述 Hello 模块为例,实现一个简单的异步加载
class FakeHello extends Component {
constructor(props){
super(props)
this.state = {
moduleLoaded:false
}
this._module = null
}
componentDidMount(){
if(!this.state.moduleLoaded){
setTimeout(()=>{
this._module= require('../hello').default
this.setState({moduleLoaded:true})
},1000)
}
}
render() {
if(!this.state.moduleLoaded){
return <div>loading</div>
}else{
let M = this._module
return <M {...this.props} />
}
}
}
同时将添加一个 button,通过在点击事件回调中修改 state.show 值来控制 Hello 模块是否展示:
<btn onClick={this.load} > {this.state.show?'off':'on'}</btn>
{this.state.show && <FakeHello title={"I'm the content"}/>}
看下效果:
可以看到第一次点击,Hello 模块显示加载中,1 秒后显示实际模块内容。第二次渲染 Hello 模块时跳过 loading,直接显示模块内容。
实验初步达到了我们的预期。
我们尝试封装一个通用模块 LazyComponent,实现对任何 React 模块的懒加载:
let _module
class LazyComponent extends Component{
constructor(props){
super(props)
this.state={
show:!!_module
}
}
componentDidMount(){
setTimeout(()=>{
_module = this.props.render()
this.setState({show:true})
},this.props.time)
}
render(){
if(!this.state.show){
return <div>will appear later</div>
}else{
return _module
}
}
}
LazyComponent 使用例子:
{
this.state.show &&
<LazyComponent time={1000} render={()=>{
let M = require('./components/hello').default
return <M title={this.state.title} />
}} />
}
LazyComponent 有 2 个属性,time 用于控制何时开始加载模块,render 表示加载具体某个模块的方法,同时返回一个基于该模块的 react element 对象。
我们再给 LazyComponet 添加 default 属性,该属性接受任何 React element 类型,为模块未加载时的默认渲染内容。
let_module
class LazyComponent extends Component{
constructor(props){
super(props)
this.state={
show:!!_module
}
}
componentDidMount(){
setTimeout(()=>{
_module = this.props.render()
this.setState({show:true})
},this.props.time)
}
render(){
if(!this.state.show){
if(this.props.default){
return this.props.default
}else{
return <div>will appear later</div>
}
}else{
return _module
}
}
}
{
this.state.show &&
<LazyComponent time={1000} default={<div>loading</div>} render={()=>{
let M = require('./components/hello').default
return <M title={this.state.title} />
}} />
}
看下效果:
看上去完美了。
但是我们发现当父容器中 title 值发生改变时,LazyComponent 包裹的 Hello 模块并没有正确更新。
Why?
我们再来看 LazyComponet render 属性,其返回的是一个包含了 props 值的 element 对象。这样当 Hello 模块首次渲染时,可以正确渲染 title 内容。但是当 LazyComponent 所在的容器 state 改变时,由于 LazyComponet 的 props 未使用 state.title 变量,React 不会重新渲染 LazyComponent 组件,LazyComponent 包裹的 Hello 组件当然也不会重新渲染。
解决办法是将所有 Hello 组件所要依赖的 state 数据通过 LazyComponent 的 props 再传递给 Hello 组件。
{
this.state.show &&
<LazyComponent time={1000} default={<div>empty</div>} realProps={{title:'hello'}} load={()=>require('./components/hello').default} />
}
let M
class LazyComponent extends Component{
constructor(props){
super(props)
this.state={
show:!!M
}
}
componentDidMount(){
if(!M){
setTimeout(()=>{
M = this.props.load()
this.setState({show:true})
},this.props.time)
}
}
render(){
if(!this.state.show){
if(this.props.default){
return this.props.default
}else{
return <div>will appear later</div>
}
}else{
return <M {...this.props.realProps} />
}
}
}
再看下效果:
现在,我们已经实现了一个简单的 LazyComponent 组件。将懒加载组件代码同普通组件比较:
<LazyComponent time={1000} default={<div>loading</div>} realProps={{title:'hello'}} load={()=>require('./components/hello').default} />
<Hello title={"hello"}/>
显而易见,虽然我们实现了懒加载,但是代码明显臃肿了很多,而且限制只能通过 realProps 传递真实 props 参数,给工程师带来记忆负担,可维护性也变差。
那么,能否更优雅的实现懒加载?能否像写普通组件的方式写懒加载组件?或者说通过工具将普通组件转换为懒加载模块?
我们想到了高阶组件(HOC),将传入组件经过包装后返回一个新组件。
于是有了下面的代码:
function lazy(loadFun,defaultRender=()=><div>loading</div>,time=17){
let _module
return class extends Component{
constructor(props){
super(props)
this.state={
show:!!_module
}
}
componentDidMount(){
let that = this
if(!_module){
setTimeout(()=>{
_module=loadFun()
that.setState({show:true})
},time)
}
}
render(){
if(!this.state.show){
return defaultRender()
}else{
let M = _module
return <M {...this.props} />
}
}
}
}
使用方法:
const LazyHello = lazy(()=>require('./components/hello').default,()=><Loading />,1000)
<LazyHello title={"I'm the content"}/>
总结
通过本次实践,我们得到了两种实现模块懒加载的解决方案:
A、使用 LazyComponent 组件,load 属性传入需要懒加载模块的加载方法;
B、使用高阶函数 lazy 包装原始组件,返回支持懒加载特性的新组件。