1. 引言

这次介绍的文章是 scheduling-in-react,简单来说就是 React 的调度系统,为了得到更顺滑的用户体验。

毕竟前端做到最后,都是体验优化,前端带给用户的价值核心就在于此。

2. 概述

文章从 Dan 在 JSConf 提到的 Demo 说起:

这是一个测试性能的 Demo,随着输入框字符的增加,下方图表展示的数据量会急速提升。在 Synchronous 与 Debounced 模式下的效果都不尽如人意,只有 Concurrent 模式下看起来是顺畅的。

那么为什么普通的 Demo 会很卡呢?

这就涉及到浏览器 Event Loop 规则了。

JS 是单线程的,浏览器同一时间只能做一件事情,而肉眼能识别的刷新频率在 60FPS 左右,这意味着我们需要在 16ms 之内完成 Demo 中的三件事:响应用户输入,做动画,Dom 渲染。

然而目前几乎所有框架都使用同步渲染模式,这意味着如果一个渲染函数执行时间超过了 16ms,则不可避免的发生卡顿。

总结一下有两个主要问题:

  1. 长时间运行的任务造成页面卡顿,我们需要保证所有任务能在几毫秒内完成,这样才能保证页面的流畅。
  2. 不同任务优先级不同,比如响应用户输入的任务优先级就高于动画。这个很好理解。

React 调度机制

为了解决这个问题,React16 通过 Concurrent(并行渲染) 与 Scheduler(调度)两个角度解决问题:

  • Concurrent: 将同步的渲染变成可拆解为多步的异步渲染,这样可以将超过 16ms 的渲染代码分几次执行。
  • Scheduler: 调度系统,支持不同渲染优先级,对 Concurrent 进行调度。当然,调度系统对低优先级任务会不断提高优先级,所以不会出现低优先级任务总得不到执行的情况。

为了保证不产生阻塞的感觉,调度系统会将所有待执行的回调函数存在一份清单中,在每次浏览器渲染时间分片间尽可能的执行,并将没有执行完的内容 Hold 住留到下个分片处理。

Concurrent 的正式 API 会在 2019 Q2 发布,现在可以通过 <React.unstable_ConcurrentMode> API 方式调用:

ReactDOM.render(
<React.unstable_ConcurrentMode>
<App />
</React.unstable_ConcurrentMode>,
rootElement
);

只申明这个是不够的,因为我们还没有申明各函数执行的优先级。我们可以通过 npm i scheduler 包来申明函数的优先级:

import { unstable_next } from "scheduler";

function SearchBox(props) {
const [inputValue, setInputValue] = React.useState(); function handleChange(event) {
const value = event.target.value; setInputValue(value);
unstable_next(function() {
props.onChange(value);
sendAnalyticsNotification(value);
});
} return <input type="text" value={inputValue} onChange={handleChange} />;
}

unstable_next() 作用域下的代码优先级是 Normal,那么产生的效果是:

  1. 如果 props.onChange(value) 可以在 16ms 内执行完,则与不使用 unstable_next 没有区别。
  2. 如果 props.onChange(value) 的执行时间过长,可能这个函数会在下次几次的 Render 中陆续执行,不会阻塞后续的高优先级任务。

调度带来的限制

调度系统也存在两个问题。

  1. 调度系统只能有一个,如果同时存在两个调度系统,就无法保证调度正确性。
  2. 调度系统能力有限,只能在浏览器提供的能力范围内进行调度,而无法影响比如 Html 的渲染、回收周期。

为了解决这个问题,Chrome 正在与 React、Polymer、Ember、Google Maps、Web Standars Community 共同创建一个 浏览器调度规范,提供浏览器级别 API,可以让调度控制更底层的渲染时机,也保证调度器的唯一性。

3. 精读

关于 React 调度系统的剖析,可以读 深入剖析 React Concurrent 这篇文章,感谢我们团队的 淡苍 提供。

简单来说,一次 Render 一般涉及到许多子节点,而 Fiber 架构在 Render 阶段可以暂停,一个一个节点的执行,从而实现了调度的能力。

React 调度能力的限制

这意味着,如果你的 React 应用目前是流畅的,开启 Concurrent 并不会对你的应用带来性能体验上的提升,如果你的 React 应用目前是卡顿的,或者在某些场景下是卡顿的,那么 Concurrent 或许可以挽救你一下,带来一些改变。

正如《深入剖析 React Concurrent》一文提到的,如果你的应用没有性能问题,就不要指望 React 调度能力有所帮助了。

这也是在说,如果一段代码逻辑不存在性能问题,就不需要使用 Concurrent 优化,因为这种优化是无效的。我们需要能分辨哪些逻辑需要优化,哪些逻辑不要。

从现在开始尝试 Function Component

为了配合 React Schedule 的实现,学会使用 Function Component 模式编写组件是很重要的,因为:

  1. Class Component 的生命周期概念阻碍了 React 调度系统对任务的拆分。
  2. 调度系统可能对 componentWillMount 重复调用,使得 Class Component 模式下很容易写出错误的代码。
  3. Function Component 遵循了更严格的副作用分离,这使得 Concurrent 执行过程不会引发意外效果。

React.lazy

与 Concurrent 一起发布的,还有 React 组件动态 import 与载入方案。正常的组件载入是这样的:

import OtherComponent from "./OtherComponent";

function MyComponent() {
return (
<div>
<OtherComponent />
</div>
);
}

但如果使用了 import() 动态载入,可以使用 React.lazy 让动态引入的组件像普通组件一样被使用:

const OtherComponent = React.lazy(() => import("./OtherComponent"));

function MyComponent() {
return (
<div>
<OtherComponent />
</div>
);
}

如果要加入 Loading,就可以配合 Suspense 一起使用:

import React, { lazy, Suspense } from "react";
const OtherComponent = lazy(() => import("./OtherComponent")); function MyComponent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
);
}

和 Concurrent 类似,React.lazy 方案也是一种对性能有益的组件加载方案。

调度分类

调度分 4 个等级:

  • Immediate:立即执行,最高优先级。
  • render-blocking:会阻塞渲染的优先级,优先级类似 requestAnimationFrame。如果这种优先级任务不能被执行,就可能导致 UI 渲染被 block。
  • default:默认优先级,普通的优先级。优先级可以理解为 setTimeout(0) 的优先级。
  • idle:比如通知等任务,用户看不到或者不在意的。

目前建议的 API 类似如下:

function mytask() {
...
} myQueue = TaskQueue.default("render-blocking")

先创建一个执行队列,并设置队列的优先级。

taskId = myQueue.postTask(myTask, <list of args>);

再提交队列,拿到当前队列的执行 id,通过这个 id 可以判断队列何时执行完毕。

myQueue.cancelTask(taskId);

必要的时候可以取消某个函数的执行。

4. 总结

随着 Hooks 的发布,即将到来的 Concurrent 与 Suspense 你是否准备好了呢?

笔者希望大家一起思考,这三种 API 会给前端开发带来什么样的改变?欢迎留言!

讨论地址是:精读《Scheduling in React》 · Issue #146 · dt-fe/weekly

如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

special Sponsors

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证

精读《Scheduling in React》的更多相关文章

  1. 精读《V8 引擎 Lazy Parsing》

    1. 引言 本周精读的文章是 V8 引擎 Lazy Parsing,看看 V8 引擎为了优化性能,做了怎样的尝试吧! 这篇文章介绍的优化技术叫 preparser,是通过跳过不必要函数编译的方式优化性 ...

  2. 深入浏览器工作原理和JS引擎(V8引擎为例)

    浏览器工作原理和JS引擎 1.浏览器工作原理 在浏览器中输入查找内容,浏览器是怎样将页面加载出来的?以及JavaScript代码在浏览器中是如何被执行的? 大概流程可观察以下图: 首先,用户在浏览器搜 ...

  3. [翻译] V8引擎的解析

    原文:Parsing in V8 explained 本文档介绍了 V8 引擎是如何解析 JavaScript 源代码的,以及我们将改进它的计划. 动机 我们有个解析器和一个更快的预解析器(~2x), ...

  4. 一文搞懂V8引擎的垃圾回收

    引言 作为目前最流行的JavaScript引擎,V8引擎从出现的那一刻起便广泛受到人们的关注,我们知道,JavaScript可以高效地运行在浏览器和Nodejs这两大宿主环境中,也是因为背后有强大的V ...

  5. Chrome V8引擎系列随笔 (1):Math.Random()函数概览

    先让大家来看一幅图,这幅图是V8引擎4.7版本和4.9版本Math.Random()函数的值的分布图,我可以这么理解 .从下图中,也许你会认为这是个二维码?其实这幅图告诉我们一个道理,第二张图的点的分 ...

  6. (译)V8引擎介绍

    V8是什么? V8是谷歌在德国研发中心开发的一个JavaScript引擎.开源并且用C++实现.可以用于运行于客户端和服务端的Javascript程序. V8设计的初衷是为了提高浏览器上JavaScr ...

  7. 浅谈Chrome V8引擎中的垃圾回收机制

    垃圾回收器 JavaScript的垃圾回收器 JavaScript使用垃圾回收机制来自动管理内存.垃圾回收是一把双刃剑,其好处是可以大幅简化程序的内存管理代码,降低程序员的负担,减少因 长时间运转而带 ...

  8. V8引擎嵌入指南

    如果已读过V8编程入门那你已经熟悉了如句柄(handle).作用域(scope)和上下文(context)之类的关键概念,以及如何将V8引擎作为一个独立的虚拟机来使用.本文将进一步讨论这些概念,并介绍 ...

  9. 浅谈V8引擎中的垃圾回收机制

    最近在看<深入浅出nodejs>关于V8垃圾回收机制的章节,转自:http://blog.segmentfault.com/skyinlayer/1190000000440270 这篇文章 ...

  10. 深入出不来nodejs源码-V8引擎初探

    原本打算是把node源码看得差不多了再去深入V8的,但是这两者基本上没办法分开讲. 与express是基于node的封装不同,node是基于V8的一个应用,源码内容已经渗透到V8层面,因此这章简述一下 ...

随机推荐

  1. 爬虫之正则和xpath

    一.正解解析 常用正则表达式回顾: 单字符: . : 除换行以外所有字符 [] :[aoe] [a-w] 匹配集合中任意一个字符 \d :数字 [-] \D : 非数字 \w :数字.字母.下划线.中 ...

  2. C# 去除\0

    在做项目的时候遇到了这种情况,如图 这个字符串包含了好多的\0,等你放大查看的时候又什么都不显示,十分惹人烦,哈哈 网上有的解决办法说 tmpStr.TrimEnd("\0");但 ...

  3. 渗透之Empire

    文中提及的部分技术可能带有一定攻击性,仅供安全学习和教学用途,禁止非法使用! Empire是一个纯碎的PowerShell后期漏洞利用代理工具,它建立在密码学.安全通信和灵活的架构之上.Empire实 ...

  4. tf.contrib.slim arg_scope

    缘由 最近一直在看深度学习的代码,又一次看到了slim.arg_scope()的嵌套使用,具体代码如下: with slim.arg_scope( [slim.conv2d, slim.separab ...

  5. python elasticsearch 批量写入数据

    from elasticsearch import Elasticsearch from elasticsearch import helpers import pymysql import time ...

  6. 拼接SQL执行语句时,对单引号的处理

    例: declare @SQL nvarchar(1000); declare @str nvarchar(100); set @str='Joe''s NB'; // 打印出来的应该是这样:Joe' ...

  7. 限制oracle某用户仅能从某IP登录

    system用户创建触发器,登录后触发检查 CREATE OR REPLACE TRIGGER system.check_ip_addresses_test AFTER logon ON DATABA ...

  8. FBOSS: Building Switch Software at Scale

    BOSS: 大规模环境下交换机软件构建 本文为SIGCOMM 2018 论文,由Facebook提供. 本文翻译了论文的关键内容. 摘要: 在网络设备(例如交换机和路由器)上运行的传统软件,通常是由供 ...

  9. [.net 面向对象程序设计深入](26)实战设计模式——策略模式 Strategy (行为型)

    [.net 面向对象程序设计深入](26)实战设计模式——策略模式 Strategy (行为型) 1,策略模式定义 策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换.策略模 ...

  10. Touch事件在移动端web开发中的详解

    一.pc端事件回顾 HTML事件.DOM0事件.DOM2事件 事件对象. 如果上述概念不清楚,请先去了解. 二.移动端事件简介 2.1 pc端事件在移动端的问题 ​ 移动设备主要特点是不配备鼠标,键盘 ...