专栏首页携程技术干货 | React模块懒加载初探

干货 | React模块懒加载初探

作者简介

常冉冉,携程租车高级前端开发工程师。拥有丰富的React技术栈及Nodejs工程实践经验,喜欢前端新技术。

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包装原始组件,返回支持懒加载特性的新组件。

本文分享自微信公众号 - 携程技术中心(ctriptech),作者:常冉冉

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-12-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 干货 | 一个数据分析师眼中的数据预测与监控

    束开亮,携程大市场部BI团队,负责数据分析与挖掘。同济应用数学硕士,金融数学方向,法国统计学工程师,主修风险管理与金融工程。

    携程技术
  • 干货 | 携程AI模型引擎设计与实践

    携程技术
  • 敏捷转型,以用户为中心——携程的项目管理实践

    移动互联网时代,产品更新迭代日新月异,企业如何永葆活力不断创新呢?在“2016中国IT项目管理大会”上,携程作为中国旅游电商首席提出,积极拥抱变化,合力前行,但...

    携程技术
  • React基础语法

    JSX语法中,可以在大括号内放置任何有效的JavaScript表达式。例如2+2、user.firstName或formatName(user)等均是有效的Ja...

    前端_AWhile
  • (转) 谈一谈创建React Component的几种方式

    原文地址:http://www.cnblogs.com/Unknw/p/6431375.html

    mafeifan
  • react学习

    JSX是一个JavaScript的语法扩展。因为JSX在语法上更简洁JavaScript而不是HTML,所以React DOM使用cameCase来定义属性的名...

    Cloud-Cloudys
  • React学习(2)——状态、事件与动态渲染 原

        上一篇文章最后说明了组件传入的参数必须是只读的,但是在丰富的前端应用中,页面样式是时时刻刻会发生变化的。在前面的章节中介绍了一个时钟的例子,通过重复调用...

    随风溜达的向日葵
  • (React 框架)React技术

      当时他们的团队在市面上没找到合适的MVC 框架,就自己写一个 JS 框架,用来架设 instagram(图片分享社交网路),2013年开源

    py3study
  • 塔荐 | 比特币突破8000美元,我们找到了用DL预测虚拟货币价格的方法

    前 言 截至 11 月 22 日,比特币的价格再创历史新高(约 1 比特币兑 8120 美元),在惊讶于虚拟货币「不可战胜」的同时,我们或许能可以从这一波热潮...

    灯塔大数据
  • 来了来了!趋势预测算法大PK!

    趋势预测在很多应用场景中都会起到至关重要的作用,比如淘宝商家会考虑库存量应该保持在多少才能够满足客户需求,商场希望得知假期会迎来多大的客流量以安排系列活动,机场...

    AI科技大本营

扫码关注云+社区

领取腾讯云代金券