1. 新增环境依赖

React 16 依赖于ES6新增集合类型 Map 和 Set。若执行环境不支持,可以使用core-js进行polyfill。

import 'core-js/es6/map';
import 'core-js/es6/set';
import React from 'react';
import ReactDOM from 'react-dom';

React16 也依赖于 requestAnimationFrame。

2. 新增客户端增量渲染能力

React16对服务端渲染做了很多优化,真正做到了:

服务端首次渲染,客户端增量渲染(目前还非常不好用)并添加事件`

React15只能做到:服务端首次渲染,客户端添加事件;使用React15时服务端渲染与客户端首次渲染的任何不一致,都会导致服务端渲染输出的DOM树被替换。

React16保证:在development模式使用React15没有任何warning时,可以无缝升级到React16。

但我们很难做到服务端渲染输出的DOM和客户端首次渲染输出的DOM完全一致;这就使得升级React16变得困难。针对这种状况,目前主要有三种升级思路:

2.1 简单升级,完全放弃新特性

基于React15时,我们往往误用renderToString,通过renderToString渲染DOM,然后在客户端通过ReactDOM.render渲染出有局部差异的DOM;然而由于React15对服务端渲染DOM和客户端渲染DOM执行严格的一致性检查,所以会用客户端渲染输出DOM替换服务端渲染输出的DOM;这种情况下使用renderToString并不比renderToStaticMarkup有性能优势。

针对这种情况,升级到React16后,我们可以仍然使用renderToStaticMarkup输出服务端DOM,客户端也会使用ReactDOM.render输出的DOM替换服务端输出的DOM;相比React15没有性能上的降级。

2.3 谨慎升级,客户端不进行增量渲染

放弃服务端渲染会导致用户可交互时间变长,并不是最优选择。在保证服务端渲染输出的DOM和客户端首次渲染输出的DOM完全一致的情况下,我们可以安全地使用React16,在客户端添加DOM事件。

针对客户端首次渲染的差异性要求,可以组件挂载后设置组件的状态来触发组件重新渲染。比如如下代码:

<div className={isInNavigator?"clientClass":"serverClass"}}>
</div>

可改为:

state={
   divClass: "serverClass"
}
componentDidMount(){
    this.setState({
        divClass: "clientClass"
    });
}
<div className={this.state.divClass}>
</div>

2.3 谨慎升级,客户端进行增量渲染

React16新增了客户端首次增量渲染的能力,但我们必须非常小心地使用它,因为它并不如我们想象般聪明。目前发现增量渲染有如下问题:

(1). 无法触发标签属性变更

图片描述

(2). 兄弟DOM结构的删除导致页面混乱

图片描述

图片描述

由于React16的客户端首次增量渲染有不少奇怪的问题,目前较为安全的做法是:

保证服务端渲染输出的DOM结构与客户端首次渲染输出的DOM结构完全一致,客户端不做增量渲染,仅添加DOM事件。

https://github.com/reactjs/react-basic/blob/master/README.md

以React服务端渲染为代表的现代化web前端技术,使得web页面的首屏时间不再依赖于JS文件的加载时间(JS文件的加载和执行只影响页面的可交互时间)。

一个典型的web架构中,首屏渲染主要经历如下几个步骤:

(1)浏览器向接入层请求HTML

(2)接入层向后台服务请求数据后通过服务端渲染将HTML返回给浏览器

(3)浏览器向CDN请求CSS后开始渲染首屏

如下图所示:

no critical css

浏览器为了渲染首屏需要两次请求:一次向接入层请求HTML文档;另外一次向CDN请求CSS文件。有没有只需一次请求即可渲染出首屏的方案呢? 通过使用内联样式,我们就可以使得浏览器不需要通过网络请求CSS;但对于一个中等规模的页面,需要内嵌的CSS内容过多,使得HTML文件本身过大,进而影响HTML本身的网络耗时,导致首屏耗时增加。我们需要一个折中方案来调和这种矛盾。

1.关键样式的作用

如果我们只是将首屏所需的关键样式内嵌到HTML页面中,然后通过网络请求获取非首屏所需的其他样式,就可以在保证一次请求即可渲染出首屏,同时尽量地减少了所需请求HTML文档的大小。

如下图所示:通过内嵌少量的首屏关键样式,我们就使得CSS和JS加载(由于此处的CSS不影响首屏,我们可以把它同JS一起放到HTML文档的末尾)不影响首屏时间了。

critical css

接下来的问题是: 我们如何准确的获取尽量小的关键样式?

2. 提取关键样式的方案

提取关键样式有多种方案:比如基于组件的方案,对于CSS in JS结构的前端项目就非常有效,只需要将首屏中所有组件的样式抽取出来即可;另外一种是基于运行时分析的方案,也就是本文提出的方案。

2.1. 使用Chrome code coverage能力提取关键样式

新版的chrome新增了code coverge能力,通过运行时分析能够准确地得知当前代码中未被使用的比例。如下图所示, 其中一个CSS文件有超过80%的代码未被使用;这也就意味着只有20%的CSS代码对于首屏渲染是有效的。

critical css

有没有程序化的手段,来获取具体哪些代码是有效的呢? 通过Chrome Debug Protocol接口,我们可以在页面执行某个时间点(比如domReady时),收集到当前已经使用到的CSS。具体伪代码如下:

//DOMContentLoaded时触发
Page.domContentEventFired(() => {
    const styleSheetIds = {};
    const styleSheetMap = {};

    CSS.takeCoverageDelta().then(rules=>{
        //获取已经被使用的CSS规则的ID
        const usedRules = rules.coverage.filter((rule) => {
            styleSheetIds[rule.styleSheetId] = 1;
            return rule.used;
        });

        //获取所有已经被使用规则片段
        Object.keys(styleSheetIds).forEach(styleSheetId => {
            stylesSheetsPromises.push(
                CSS.getStyleSheetText({
                    styleSheetId: styleSheetId
                }).then(stylesheet => {
                    styleSheetMap[styleSheetId] = stylesheet.text || '';
                })
            )
        })

        //根据规则片段拼接出关键css文本
        for (const usedRule of usedRules) {
            const curStyleId = usedRule.styleSheetId;
            const styleSeg = styleSheetMap[curStyleId].slice(usedRule.startOffset, usedRule.endOffset);
            //收集关键样式
            cirticalCssArr.push(styleSeg);
        }
        const cirticalStyle = cirticalCssArr.join('');

    })
})

以上只截取了部分代码,全部代码请参考critical.js :

虽然我们已经能够通过Chrome Debug Protocol来获取关键样式,但该关键样式仍然无法直接应用于生产环境。一个主要的原因是它丢失了Media Query信息;Chrome Code Coverage能力只能针对特定的User Agent工作,这就使得我们获取的关键样式不具有普适性。我们需要额外的解决方案。

2.2 使用Postcss获取关键样式中的Media Query

postcss作为业界最强大的CSS处理器,借助于它我们可以结构化地处理CSS,比如抽取关键样式中Media Query。通过结合Code Coverage和postcss,我们就能获取到准确的、UA无关地关键样式了。

post critical css

核心代码如下所示:

//解析全部css
const allStyleParser = postCss.parse(allStyle);
//结构化地遍历CSS
allStyleParser.walkAtRules(rule => {
    let isCritical = false;
    //遍历Media Query表达式
    if (rule.name.match(/^media/)) {
        //只选取关键的media query
        rule.each((subRule) => {
            const selector = subRule.selector;
            if(cirticalStyle.indexOf(selector) > 0){
                isCritical = true;
            }else{
                subRule.remove();
            }
        });
        if(isCritical){
            mediaQueriesCss += rule.toString();
        }
    }
})
//关键样式+MediaQuery为最终首屏核心样式
const coveredStyle = cirticalStyle + mediaQueriesCss;

以上只截取了部分代码,全部代码请参考critical.js :

本文提出的关键样式提取方案只适用于形态相对稳定的页面(尤其适合静态页面);对于千人千面的个性化页面,可以尝试其他方案(基于组件的关键样式提取)。

https://www.gideonpyzer.com/blog/runtime-coverage-using-chrome-devtools/

https://github.com/cyrus-and/chrome-remote-interface

https://github.com/foio/chromeCriticalCss

https://github.com/postcss/postcss