资讯专栏INFORMATION COLUMN

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

codecook / 3075人阅读

摘要:往往纯的单页面应用一般不会太复杂,所以这里不引入和等等,在后面复杂的跨平台应用中我会将那些技术一拥而上。构建极度复杂,超大数据的应用。

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


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

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

手写React优化脚手架带项目

react-ssr的源码

手写Node.js原生静态资源服务器

跨平台Electron的demo

原生浏览器环境:

原生浏览器环境其实是最考验前端工程师能力的编程环境,因为我们前端大部分一开始面向浏览器编程,现在很多很多工作5-10年的前端,性能面板API都不知道用,怎么看调用函数分析耗时都不知道,这也是最近面试的情况,觉得有人说35岁失业的情况,是普遍存在,但是很大部分是你在混啊兄弟。

原生浏览器环境中使用React框架,比较常见的是制作单页面SPA应用:
原生的SPA应用,分以下几种:

CSR渲染(客户端渲染)

SSR渲染(服务端渲染)

混合渲染(预渲染,webpack的插件预渲染,Next.js的约定式路由SSR,或者使用Node.js做中间件,做部分SSR,加快首屏渲染,或者指定路由SSR.)

下面会分别仔细介绍这几种渲染形式的精细化渲染,以及优缺点:
CSR渲染

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

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;
    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(
 `
   
     
       hello
     
     
       

hello world

` ) }) 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(
    
      {renderRoutes(routers)}
    
  );
  return `
      
        
          ssr123
        
        
          
${content}
`; };

数据注水,脱水,保持客户端和服务端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(
    
      {renderRoutes(routers)}
    
  );

client.js 

 
      {renderRoutes(routers)}
    

后续可能有利用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也有它的一套生命周期

通常我们如果要使用 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"
  //这里配置表示占位符的样式~。
          


记得在移动端的滑动屏幕或者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 
this component is lazyloaded by default!
; } }
大数据React渲染,拥有让应用拥有60FPS -非常核心的一点优化

List长列表


]

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个元素;它们当中任意一个的值发生改变时,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的新版本调度阶段是拥有三根指针的可中断的链表遍历,所以这样既不影响下面的遍历,也不影响用户交互等行为。

使用requestAnimationFrame,当页面处于未激活的状态下,该页面的屏幕刷新任务会被系统暂停,由于requestAnimationFrame保持和屏幕刷新同步执行,所以也会被暂停。当页面被激活时,动画从上次停留的地方继续执行,节约 CPU 开销。

一个刷新间隔内函数执行多次时没有意义的,因为显示器每 16.7ms 刷新一次,多次绘制并不会在屏幕上体现出来

在高频事件(resize,scroll等)中,使用requestAnimationFrame可以防止在一个刷新间隔内发生多次函数执行,这样保证了流畅性,也节省了函数执行的开销

某些情况下可以直接使用requestAnimationFrame替代 Throttle 函数,都是限制回调函数执行的频率

使用requestAnimationFrame也可以更好的让浏览器保持60帧的动画

requestIdleCallback,这个API目前兼容性不太好,但是在Electron开发中,可以使用,两者还是有区别的,而且这两个api用好了可以解决很多复杂情况下的问题~。当然你也可以用上面的api封装这个api,也并不是很复杂。

当关注用户体验,不希望因为一些不重要的任务(如统计上报)导致用户感觉到卡顿的话,就应该考虑使用requestIdleCallback。因为requestIdleCallback回调的执行的前提条件是当前浏览器处于空闲状态。

图中一帧包含了用户的交互、js的执行、以及requestAnimationFrame的调用,布局计算以及页面的重绘等工作。

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

使用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文件延迟加载~

普通的脚本

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

script标签,加上defer标签,延迟加载,但是必须在所有脚本加载完毕后才会加载它,但是这个标签有bug,不确定能否准时加载。一般只给一个

写这篇时间太耗时间,而且论坛的在线编辑器到了内容很多的时候,非常卡,React-native的以及一些细节,后面再补充
下面给出一些源码和资料地址:

手写React优化脚手架带项目

react-ssr的源码

手写Node.js原生静态资源服务器

跨平台Electron的demo

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/105905.html

相关文章

  • 如何优化你的大型React应用原创精读

    摘要:往往纯的单页面应用一般不会太复杂,所以这里不引入和等等,在后面复杂的跨平台应用中我会将那些技术一拥而上。构建极度复杂,超大数据的应用。 showImg(https://segmentfault.com/img/bVbvphv?w=1328&h=768); React为了大型应用而生,Electron和React-native赋予了它构建移动端跨平台App和桌面应用的能力,Taro则赋...

    cfanr 评论0 收藏0
  • 如何优化你的大型React应用原创精读

    摘要:往往纯的单页面应用一般不会太复杂,所以这里不引入和等等,在后面复杂的跨平台应用中我会将那些技术一拥而上。构建极度复杂,超大数据的应用。 showImg(https://segmentfault.com/img/bVbvphv?w=1328&h=768); React为了大型应用而生,Electron和React-native赋予了它构建移动端跨平台App和桌面应用的能力,Taro则赋...

    xiguadada 评论0 收藏0
  • 论一个前端工程师如何快速学习,成长。准备自己的35岁 【-原创精读

    showImg(https://segmentfault.com/img/bVbw3tK?w=1240&h=827); 前端工程师这个岗位,真的是反人性的 我们来思考一个问题: 一个6年左右经验的前端工程师: 前面两年在用jQuery 期间一直在用React-native(一步一步踩坑过来的那种) 最近两年还在写微信小程序 下面一个2年经验的前端工程师: 并不会跨平台技术,他的两年工作都是Reac...

    RdouTyping 评论0 收藏0
  • The Cost Of JavaScript 2018 精读

    摘要:目前我们的业务项目采用的来进行优化和首屏性能提升。可变性需要让开发人员降低开发时的基准线,来保证每一个用户的体验。对于路由的切分以及库的引入来说,这一个原则至关重要。快速生成一份站点的性能审查报告。 The Cost Of JavaScript 2018 关于原文 原文是在Medium上面看到的,Chrome工程师Addy Osmani发布的一篇文章,这位的Medium上面的自我介绍里...

    lushan 评论0 收藏0
  • 学习资料 - 收藏集 - 掘金

    摘要:以下是通过年前端程序员必知单页面应用的核心前端掘金这几年里,单页面应用的框架令人应接不暇,各种新的概念也层出不穷。 实战实现一个h5转盘抽奖页面,谈谈代码实现,顺便谈一下优化和数据处理 - 掘金代码地址 前言 这个组件是我写过的关于移动端h5活动转盘抽奖的页面,当时写完之后确定挺好看、挺炫的,所以就把它单独出来了,以后再写类似的页面,可以参考其中的一些实现原理! 主要用到的技术 用Ma...

    marser 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<