如何优化你的超大型React应用 【原创精读】

clipboard.png

React为了大型应用而生, ElectronReact-native赋予了它构建移动端跨平台 App和桌面应用的能力, Taro则赋予了它一次编写,生成多种平台小程序和 React-native应用的能力,这里特地说下 Taro,它是国产,文档写得比较不错,并且它的升级速度比较快,有 issue我看也会及时解决,他们的维护人员仍是很是敬业的!


clipboard.pngcss

  • Tips:本文某些知识点若是介绍不对或者不全的地方欢迎指出,本文可能内容比较多,阅读时间花费比较长,可是但愿你能够认真看下去,能够的话最好手把手去实现一些code,本文全部代码均手写。

本文会从原生浏览器环境,到跨平台开发逐渐去深刻介绍,先给一些资料

原生浏览器环境:

  • 原生浏览器环境实际上是最考验前端工程师能力的编程环境,由于咱们前端大部分一开始面向浏览器编程,如今不少不少工做5-10年的前端,性能面板API都不知道用,怎么看调用函数分析耗时都不知道,这也是最近面试的状况,以为有人说35岁失业的状况,是广泛存在,可是很大部分是你在混啊兄弟。
原生浏览器环境中使用React框架,比较常见的是制做单页面SPA应用:
原生的SPA应用,分如下几种:
  • CSR渲染(客户端渲染)
  • SSR渲染(服务端渲染)
  • 混合渲染(预渲染,webpack的插件预渲染,Next.js的约定式路由SSR,或者使用Node.js作中间件,作部分SSR,加快首屏渲染,或者指定路由SSR.)
下面会分别仔细介绍这几种渲染形式的精细化渲染,以及优缺点:

CSR渲染

  • 客户端请求RestFul接口,接口吐回静态资源文件html

    • Node.js实现代码
const express = require('express')
const app = express()

app.use(express.static('pulic'))//这里的public就是静态资源的文件夹,让客户端拉取的,这里的代码是前端的代码已经构建完毕的代码 

app.get('/',(req,res)=>{
 //do something 
    
})

app.listen(3000,err=>{
    if(!err)=>{
        console.log('监听端口号3000成功')
    }
})
  • 客户端收到一个HTML文件,和若干个CSS文件,以及多个javaScript文件
  • 用户输入了url地址栏而后客户端返回静态文件,客户端开始解析
  • 客户端解析文件,js代码动态生成页面。(这也是为何说单页面应用的SEO不友好的缘由,初始它只是一个空的div标签的HTML文件)
  • 判断一个页面是否是CSR,很大程度上能够根据右键点开查看页面元素,若是只有一个空的div标签,那么大几率能够说是单页面,CSR,客户端渲染的网页。
CSR的应用,如何精细化渲染呢?

单页面采起CSR形式,大都依赖框架,VueReact之类。一旦使用这类型技术架构,状态数据集中管理,单向数据流,不可变数据,路由懒加载,按需加载组件,适当的缓存机制(PWA技术),细致拆分组件,单一数据来源刷新组件,这些都是咱们能够精细化的方向。每每纯CSR的单页面应用通常不会太复杂,因此这里不引入PWAweb work等等,在后面复杂的跨平台应用中我会将那些技术蜂拥而上。

  • 单一数据来源决定组件是否刷新是精细化最重要的方向。
class app extends React.PureComponent{

    ///////
}

export default connect(
 (({xx,xxx,xxxx,xxxxx}))
////

)(app)
一旦业务逻辑很是复杂的状况下,假设咱们使用的是 dva集中状态管理,同时链接这么多的状态树模块,那么可能会形成状态树模块中任意的数据刷新致使这个组件被刷新,可是其实这个组件此时是不须要刷新的。
  • 这里能够将须要的状态经过根组件用props传入,精确刷新的来源,单一可变数据来源追溯性强,也更方便debug
  • 单向数据流不可变数据,经过immutable.js这个库实现
import Immutable from require('immutable');
    var map1: Immutable.Map<string, number>;
    map1 = Immutable.Map({ a: 1, b: 2, c: 3 });
    var map2 = map1.set('b', 50);
    map1.get('b'); // 2
    map2.get('b'); // 50
不可变数据,数据共享,持久化存储,经过 is比较,每次 map生成的都是惟一的 ,它们比较的是 codehash的值,性能比经过递归或者直接比较强不少。在 PureComponent浅比较很差用的时候
  • 通常的组件,使用PureComponent减小重复渲染便可
  • PureComponent,平时咱们建立 React 组件通常是继承于 Component,而 PureComponent 至关因而一个更纯净的 Component,对更新先后的数据进行了一次浅比较。只有在数据真正发生改变时,才会对组件从新进行 render。所以能够大大提升组件的性能。
  • PureComponent部分源码,其实就是浅比较,只不过对一些特殊值进行了判断:
function is(x: any, y: any) {
    return (
        (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) 
    );
}
这里特别注意,为何使用immutable.js和pureComponent,由于React一旦根组件被刷

新,会自上而下逐渐刷新整个子孙组件,这样性能损耗重复渲染就会多出不少,因此咱们不只要单一数据来源控制组件刷新,偶尔还须要在shouldComponentUpdate中对比nextProps和this.props 以及this.state以及nextState.前端

  • 路由懒加载+code-spliting,加快首屏渲染,也能够减轻服务器压力,由于不少人可能访问你的网页并不会看某些路由的内容
  • 使用react-loadable,支持SSR,很是推荐,官方的lazy不支持SSR,这是一个遗憾,这里须要配合wepback4optimization配置,进行代码分割
Tips:这里须要下载支持动态 importbabel预设包 @babel/plugin-syntax-dynamic-import ,它支持动态倒入组件
webpack配置:

 optimization: {
        runtimeChunk: true,
        splitChunks: {
            chunks: 'all'
        }
    }
import React from 'react'
    import Loading from './loading-window'//占位的那个组件,初始加载
    import Loadable from 'react-loadable'
    const LoadableComponent = Loadable({
        loader: () => import('./sessionWindow'),//真正须要加载的组件
        loading: Loading,
      });
      
      
    export default LoadableComponent
  • 好了,如今路由懒加载组件以及代码分割已经作好了,并且它支持SSR。很是棒
  • 因为纯CSR的网页通常不是很复杂,这里再介绍一个方面,那就是,能不用redux,dva等集中状态管理的状态就不上状态树,实践证实,频繁更新状态树对用户体验来讲是影响很是大的。这个异步的过程,更耗时。远不如支持经过props等方式进行组件间通讯,原则上除了不少组件共享的数据才上状态树,不然都采用其余方式进行通讯。

SSR,服务端渲染:

服务端渲染能够分为:
纯服务端渲染,如jade,tempalte,ejs等模板引擎进行渲染,而后返回给前端对应的HTML文件
  • 这里也使用Node.js+express框架
const express= require('express')
const app =express()
const jade = require('jade')
const result = ***
const url path = *** 
const html = jade.renderFile(url, { data: result, urlPath })//传入数据给模板引擎
app.get('/',(req,res)=>{
    res.send(html)//直接吐渲染好的`html`文件拼接成字符串返回给客户端
}) //RestFul接口 

app.listen(3000,err=>{
    //do something
})
混合渲染,使用webpack4插件,预渲染指定路由,被指定的路由为SSR渲染,后台0代码实现
const PrerenderSPAPlugin = require('prerender-spa-plugin')
new PrerenderSPAPlugin({
            routes: ['/','/home','/shop'],
            staticDir: resolve(__dirname, '../dist'),
          }),
混合渲染,使用Node.js做为中间件,SSR指定的路由加快首屏渲染,固然CSS也能够服务端渲染,动态Title和meta标签,更好的SEO优化,这里Node.js还能够同时处理数据,减轻前端的计算负担。
  • 我以为掘金上的神三元那篇文章就写得很好,后面我本身去逐步实现了一次,感受对SSR对理解更为透彻,加上原本就天天在写Node.js,还会一点Next,Nuxt,服务端渲染,以为大同小异。
  • 服务端渲染本质,在服务端把代码运行一次,将数据提早请求回来,返回运行后的html文件,客户端接到文件后,拉取js代码,代码注水,而后显示,脱水,js接管页面。
  • 同构直出代码,能够大大下降首屏渲染时间,通过实践,根据不一样的内容和配置能够缩短40%-65%时间,可是服务端渲染会给服务器带来压力,因此折中根据状况使用。
  • 如下是一个最简单的服务端渲染,服务端直接吐拼接后的html结构字符串:
var express = require('express')
var app = express()

app.get('/', (req, res) => {
 res.send(
 `
   <html>
     <head>
       <title>hello</title>
     </head>
     <body>
       <h1>hello world </h1>
     </body>
   </html>
 `
 )
})

app.listen(3000, () => {
 if(!err)=>{
console.log('3000监听')Ï
}
})
只要客户端访问 localhost:3000就能够拿到数据页面访问
服务端渲染核心,保证代码在服务端运行一次,将reduxstore状态树中的数据一块儿返回给客户端,客户端脱水,渲染。 保证它们的状态数据和路由一致,就能够说是成功了。必需要客户端和服务端代码和数据一致性,不然SSR就算失败。
//server.js

// server/index.js
import express from 'express';
import { render } from '../utils';
import { serverStore } from '../containers/redux-file/store';
const app = express();
app.use(express.static('public'));
app.get('*', function(req, res) {
  if (req.path === '/favicon.ico') {
    res.send();
    return;
  }
  const store = serverStore();
  res.send(render(req, store));
});
const server = app.listen(3000, () => {
  var host = server.address().address;
  var port = server.address().port;
  console.log(host, port);
  console.log('启动链接了');
});


//render函数
import Routes from '../Router';
import { renderToString } from 'react-dom/server';
import { StaticRouter, Link, Route } from 'react-router-dom';
import React from 'react';
import { Provider } from 'react-redux';
import { renderRoutes } from 'react-router-config';
import routers from '../Router';
import { matchRoutes } from 'react-router-config';
export const render = (req, store) => {
  const matchedRoutes = matchRoutes(routers, req.path);
  matchedRoutes.forEach(item => {
    //若是这个路由对应的组件有loadData方法
    if (item.route.loadData) {
      item.route.loadData(store);
    }
  });
  console.log(store.getState(),Date.now())
  const content = renderToString(
    <Provider store={store}>
      <StaticRouter location={req.path}>{renderRoutes(routers)}</StaticRouter>
    </Provider>
  );
  return `
      <html>
        <head>
          <title>ssr123</title>
        </head>
        <body>
          <div id="root">${content}</div>
          <script>window.context={state:${JSON.stringify(store.getState())}}</script>
          <script src="/index.js"></script>
        </body>
      </html>
    `;
};
  • 数据注水,脱水,保持客户端和服务端store的一致性。
上面返回的 script标签,里面已经注水,将在服务端获取到的数据给到了全局window下的context属性,在初始化客户端 store时候咱们给它脱水。初始化渲染使用服务端获取的数据~
import thunk from 'redux-thunk';
import { createStore, applyMiddleware } from 'redux';
import reducers from './reducers';

export const getClientStore = () => {
  const defaultState = window.context ? window.context.state : {};
  return createStore(reducers, defaultState, applyMiddleware(thunk));
};

export const serverStore = () => {
  return createStore(reducers, applyMiddleware(thunk));
};
  • 这里注意,在组件的componentDidMount生命周期中发送ajax等获取数据时候,先判断下状态树中有没有数据,若是有数据,那么就不要重复发送请求,致使资源浪费。
  • 多层级路由SSR
//路由配置文件,改为这种方式
import Home from './containers/Home';
import Login from './containers/Login';
import App from './containers/app';
export default [
  {
    component: App,
    routes: [
      {
        path: '/',
        component: Home,
        exact: true,
        loadData: Home.loadData
      },
      {
        path: '/login',
        component: Login,
        exact: true
      }
    ]
  }
];
  • 入口文件路由部分改为:
server.js

 const content = renderToString(
    <Provider store={store}>
      <StaticRouter location={req.path}>{renderRoutes(routers)}</StaticRouter>
    </Provider>
  );

client.js 

 <Provider store={store}>
      <BrowserRouter>{renderRoutes(routers)}</BrowserRouter>
    </Provider>
  • 后续可能有利用loader进行CSS的服务端渲染以及helmet的动态meta, title标签进行SEO优化等,今天时间紧促,就不继续写SSR了。

构建Electron极度复杂,超大数据的应用。

须要用到技术,sqlite,PWA,web work,原生Node.js,react-window,react-lazyload,C++插件等
  • 第一个提到的是sqlite,嵌入式关系型数据库,轻量型无入侵性,标准的sql语句,这里不作过多介绍。
  • PWA,渐进性式web应用,这里使用webpack4的插件,进行快速使用,对于一些数据内容不须要存储数据库的,可是却想要一次拉取,屡次复用,那么可使用这个配置

serverce work也有它的一套生命周期

clipboard.png

  • 一般咱们若是要使用 Service Worker 基本就是如下几个步骤:
  • 首先咱们须要在页面的 JavaScript 主线程中使用 serviceWorkerContainer.register() 来注册 Service Worker ,在注册的过程当中,浏览器会在后台启动尝试 Service Worker 的安装步骤。
  • 若是注册成功,Service Worker 在 ServiceWorkerGlobalScope 环境中运行; 这是一个特殊的 worker context,与主脚本的运行线程相独立,同时也没有访问 DOM 的能力。
  • 后台开始安装步骤, 一般在安装的过程当中须要缓存一些静态资源。若是全部的资源成功缓存则安装成功,若是有任何静态资源缓存失败则安装失败,在这里失败的没关系,会自动继续安装直到安装成功,若是安装不成功没法进行下一步 — 激活 Service Worker。
  • 开始激活 Service Worker,必需要在 Service Worker 安装成功以后,才能开始激活步骤,当 Service Worker 安装完成后,会接收到一个激活事件(activate event)。激活事件的处理函数中,主要操做是清理旧版本的 Service Worker 脚本中使用资源。
  • 激活成功后 Service Worker 能够控制页面了,可是只针对在成功注册了 Service Worker 后打开的页面。也就是说,页面打开时有没有 Service Worker,决定了接下来页面的生命周期内受不受 Service Worker 控制。因此,只有当页面刷新后,以前不受 Service Worker 控制的页面才有可能被控制起来。
直接上代码,存储全部 js文件和图片 //实际的存储根据自身须要,并非越多越好。
const WorkboxPlugin = require('workbox-webpack-plugin')
new WorkboxPlugin.GenerateSW({
            clientsClaim: true,
            skipWaiting: true,
            importWorkboxFrom: 'local',
            include: [/\.js$/, /\.css$/, /\.html$/, /\.jpg/, /\.jpeg/, /\.svg/, /\.webp/, /\.png/],
        }),
  • PWA并不只仅这些功能,它的功能很是强大,有兴趣的能够去lavas看看,PWA技术对于常常访问的老客户来讲,首屏渲染提高很是大,特别在移动端,能够添加到桌面保存。666啊~,在pc端更多的是缓存处理文件~
  • 使用react-lazyload,懒加载你的视窗初始看不见的组件或者图片。
/开箱即用的懒加载图片
import LazyLoad from 'react-lazyload'
 <LazyLoad height={42} offset={100} once> //这里配置表示占位符的样式~。
          <img
            src={this.state.src}
            onError={this.handleError.bind(this)}
            className={className || 'avatar'}
          />
</LazyLoad>

记得在移动端的滑动屏幕或者PC端的调用forceCheck,动态计算元素距离视窗的位置而后决定是否显示真的图片~

import { forceCheck } from 'react-lazyload';
forceCheck()
  • 懒加载组件
import { lazyload } from 'react-lazyload';
//跟上面同理,不过是一个装饰器,高阶函数而已。同样须要forcecheck()
@lazyload({
  height: 200,
  once: true,
  offset: 100
})
class MyComponent extends React.Component {
  render() {
    return <div>this component is lazyloaded by default!</div>;
  }
}

大数据React渲染,拥有让应用拥有60FPS -很是核心的一点优化

  • List长列表

clipboard.png
]java

  • react-virtualized-auto-sizer和windowScroll配合一块儿使用,达到页面复杂效果+大数据渲染保持60FPS。上面的官网里有介绍这些组件~

高计算量的工做交给web wrok线程

var myWorker = new Worker('worker.js'); 
first.onchange = function() {
  myWorker.postMessage([first.value,second.value]);
  console.log('Message posted to worker');
}

second.onchange = function() {
  myWorker.postMessage([first.value,second.value]);
  console.log('Message posted to worker');
}
  • 这段代码中变量first和second表明2个<input>元素;它们当中任意一个的值发生改变时,myWorker.postMessage([first.value,second.value])会将这2个值组成数组发送给worker。你能够在消息中发送许多你想发送的东西。
  • 在worker中接收到消息后,咱们能够写这样一个事件处理函数代码做为响应(worker.js):
onmessage = function(e) {
  console.log('Message received from main script');
  var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
  console.log('Posting message back to main script');
  postMessage(workerResult);
}
  • onmessage处理函数容许咱们在任什么时候刻,一旦接收到消息就能够执行一些代码,代码中消息自己做为事件的data属性进行使用。这里咱们简单的对这2个数字做乘法处理并再次使用postMessage()方法,将结果回传给主线程。
  • 回到主线程,咱们再次使用onmessage以响应worker回传的消息:
myWorker.onmessage = function(e) {
  result.textContent = e.data;
  console.log('Message received from worker');
}
  • 在这里咱们获取消息事件的data,而且将它设置为result的textContent,因此用户能够直接看到运算的结果。
  • 注意: 在主线程中使用时,onmessage和postMessage() 必须挂在worker对象上,而在worker中使用时不用这样作。缘由是,在worker内部,worker是有效的全局做用域。
  • 注意: 当一个消息在主线程和worker之间传递时,它被复制或者转移了,而不是共享。
开启 web work线程,其实也会损耗必定的主线程的性能,可是大量计算的工做交给它也何尝不可,其实 Node.jsjavaScript都不适合作大量计算工做,这点有目共睹,尤为是 js引擎和 GUI渲染线程互斥的状况存在。

充分合理利用ReactFeber架构diff算法优化项目

  • requestAnimationFrame调用高优先级任务,中断调度阶段的遍历,因为React的新版本调度阶段是拥有三根指针的可中断的链表遍历,因此这样既不影响下面的遍历,也不影响用户交互等行为。

clipboard.png

  • 使用requestAnimationFrame,当页面处于未激活的状态下,该页面的屏幕刷新任务会被系统暂停,因为requestAnimationFrame保持和屏幕刷新同步执行,因此也会被暂停。当页面被激活时,动画从上次停留的地方继续执行,节约 CPU 开销。
  • 一个刷新间隔内函数执行屡次时没有意义的,由于显示器每 16.7ms 刷新一次,屡次绘制并不会在屏幕上体现出来
  • 在高频事件(resize,scroll等)中,使用requestAnimationFrame能够防止在一个刷新间隔内发生屡次函数执行,这样保证了流畅性,也节省了函数执行的开销

某些状况下能够直接使用requestAnimationFrame替代 Throttle 函数,都是限制回调函数执行的频率react

使用 requestAnimationFrame也能够更好的让浏览器保持60帧的动画
  • requestIdleCallback,这个API目前兼容性不太好,可是在Electron开发中,可使用,二者仍是有区别的,并且这两个api用好了能够解决不少复杂状况下的问题~。固然你也能够用上面的api封装这个api,也并非很复杂。

clipboard.png

  • 当关注用户体验,不但愿由于一些不重要的任务(如统计上报)致使用户感受到卡顿的话,就应该考虑使用requestIdleCallback。由于requestIdleCallback回调的执行的前提条件是当前浏览器处于空闲状态。
  • 图中一帧包含了用户的交互、js的执行、以及requestAnimationFrame的调用,布局计算以及页面的重绘等工做。

假如某一帧里面要执行的任务很少,在不到16ms(1000/60)的时间内就完成了上述任务的话,那么这一帧就会有必定的空闲时间,这段时间就刚好能够用来执行requestIdleCallback的回调,以下图所示:webpack

clipboard.png

使用preloadprefetch,dns-prefetch等指定提早请求指定文件,或者根据状况,浏览器自行决定是否提早dns预解析或者按需请求某些资源。

  • 这里也能够webpack4插件实现,目前京东在使用这个方案~
const PreloadWebpackPlugin = require('preload-webpack-plugin')
 new PreloadWebpackPlugin({
            rel: 'preload',
            as(entry) {
              if (/\.css$/.test(entry)) return 'style';
              if (/\.woff$/.test(entry)) return 'font';
              if (/\.png$/.test(entry)) return 'image';
              return 'script';
            },
            include:'allChunks'
            //include: ['app']
          }),

对指定js文件延迟加载~

  • 普通的脚本

clipboard.png

  • script标签,加上async标签,遇到此标签,先去请求,可是不阻塞解析html等文件~,请求回来就立马加载

clipboard.png

  • script标签,加上defer标签,延迟加载,可是必须在全部脚本加载完毕后才会加载它,可是这个标签有bug,不肯定可否准时加载。通常只给一个

clipboard.png

写这篇时间太耗时间,并且论坛的在线编辑器到了内容不少的时候,很是卡, React-native的以及一些细节,后面再补充

下面给出一些源码和资料地址: