最近项目一直在使用 Monaco Editor 这个库. 在我加了一个新功能之后, 整个编辑器开始变的非常卡, 我试图解决这个性能问题. 但是发现有一些棘手...

评论以及文末有更新

场景

其实我加的新功能很简单, 以 vscode 为例, 他底部有一个 status bar, 用于显示当前编辑器的一些信息. 我当时加的功能是模仿 vscode, 在底部增加一个能显示当前光标位置/选中位置, 以及选中的单词的长度. 由于是公司代码的原因我这里只是简单放一下伪代码, 实际代码会比这个这个伪代码复杂和抽象很多

文件结构:

<App />// ... other components<div><Explorer /><Editor /></div> <StatusBar />

简单说明一下, 有一个组件叫 App, 由于历史遗留原因这二组件是个 class 组件, 而且代码很多, 这个组件下有非常多的子组件, EditorStatusBar 就是其中的两个子组件

而我现在的需求是: 用户在 Editor 里做任何操作, StatusBar 组件都需要显示出对应用户光标的位置

我思路也很简单:

  • cursor(光标) 部分的状态在 Editor 中是可以拿到的
  • 由于 StatusBar 这个组件需要根据 cursor 状态来渲染, 因此做状态提升, 到 App
  • App 中初始化 cursor 状态, 传入相关 setState() 回调函数到 Editor 中, Editor 组件调用更新 cursor 状态, 最后 StatusBar 获取 cursor 最新的状态进行渲染.

实现

所以代码可能长这样

// App.jsimport Editor from './Editor'
import StatusBar from './StatusBar'export default class App {constructor(props) {super(props) {this.state = {// ...other statecursorPosition: {lineNumber: 1,column: 1}}this.editorRef = React.createRef()}}setCursorPosition = (cursorPosition) => {this.setState({cursorPosition,})}render() {return (// ... other components<div><Explorer /><Editor setCursorPosition={this.setCursorPosition} editorRef={this.editorRef}/></div> <StatusBar cursorPosition={this.cursorPosition} />)}
}// Editor.jsimport MonacoEditor from 'some-thirdparty-react-monaco-package'export default function Editor(props) {const { setCursorPosition, editorRef } = propsconst handleEditorDidMount = () => {editorRef.current.onDidChangeCursorPosition(ev => {setCursorPosition(ev.position)})}return (<MonacoEditor editorDidMount={handleEditorDidMount} />)
}// StatusBar.jsimport Button from '@material-ui/core/Button';export default function StatusBar(props) {const { cursorPosition } = propsreturn (<Button size="small">{cursorPosition}</Button>)
}

看上去好像没啥问题, 我当时这么写也没考虑太多

问题

实际上最后写完我测试发现了很大的性能问题

其实很简单, 我每次在 Editor 里面调用父组件(App) 的 setState(), 都会导致父组件重新渲染. 而 App 这个组件是一个很大的类组件, 里面还渲染很多别的组件, 而用户在编辑器里面只要敲一点东西, 光标几乎都会改变(或者直接点, 用户闲着没事在编辑器里直接乱点一通, 也能达到相同的效果). 这直接导致重新渲染 App 的频率相当频繁...

然后我页面就卡爆了...

我当时有点懵, 因为说实话这种渲染确实好像没法避免, 我每次的 cursor 状态确实不一样, 我没法直接通过 shouldComponentUpdate 来避免不必要的重复渲染

当然有时候可以避免, 就是当用户的每次都点击同一个地方, cursorPosition 就一样了...

方法

我一开始想到的一个办法是, 将 Editor组件和 StatusBar 重新写在一个新的组件, 这样所有的状态就只在这个新组件(可以看成这是一个中间组件)里面管理, 不会触发 App 这个大组件的重新渲染了.

但我还是很快放弃了这个想法, 因为把 StatusBarEditor 放一起其实不简单.... 从 dom 上来看, 他们其实不是严格的兄弟组件, 中间还有着别的组件, 如果要抽出来还必须连带着别的组件一起重写:

export default function MyNewComponent(props) {const { editorRef,// ...还有很多别组件的 props...} = props// ...return (<div><CompA /><CompB />// ...other component<div><Explorer /><Editor editorRef={editorRef} /></div><StatusBar /><div>)
}

总之, 我这么重构 effort 其实挺大...

可能的解决方案?

后来和另一个组里的实习生交流的时候, 他提出尝试把 cursor 部分的状态放到 redux 里面管理, 在 Editor 里面 dispatch 相应改变 cursorPositionaction, 在 StatusBar 里面连接 redux 拿到最新的 cursorPosition 状态. 这样直接绕过 App 这一层, 避免了重复渲染

其实这个思路和之前的想法类似 都是要避开 App 这个大组件, 只不过用 redux 这种状态管理库似乎代码写起来简单一些

代码最后就变成这样了:

// Editor.jsimport { useDispatch } from 'react-redux'
import { setCursorPosition } from './editorActions.js'
import MonacoEditor from 'some-thirdparty-react-monaco-package'export default function Editor(props) {const { setCursorPosition, editorRef } = propsconst dispatch = useDispatch()const handleEditorDidMount = () => {editorRef.current.onDidChangeCursorPosition(ev => {dispatch(setCursorPosition(ev.position))})}return (<MonacoEditor editorDidMount={handleEditorDidMount} />)
}// StatusBar.jsimport Button from '@material-ui/core/Button';
import { useSelector, shallowEqual } from 'react-redux'export default function StatusBar(props) {const { cursorPosition } = useSelector(state => state.editor, shallowEqual)return (<Button size="small">{cursorPosition}</Button>)
}

不过这么讲也存在别的一些问题:

  • cursorPosition 这个状态只在一个组件里被用到, Redux 本意还是为了做状态的管理, 多个组件可能都会共享到这个状态, 但现在只有一个, 似乎有点大材小用了
  • 我们项目里 Redux 已经放了很多的状态了, mentor 不希望我再放别的进去了...
  • 我没见过像我一样用 Redux 来做性能优化的...

可能有更好的解法?

如果你恰好能明白我在讲什么, 并且有更好的办法, 请务必告诉我...


更新

文章发布之后有几位前辈评论了一下, 都非常好. 我自己总结然后实践了一下, 以下是我的解决思路

不管是我之前用的 Redux, 还是评论里面提到的 Context, 其实都是 Pub/Sub (发布订阅)这个思想的实践. 不过由于目前使用 Redux 有点太重, 所以其实用 Context 会更好

不过在使用 Context 的时候存在一个问题: 就是如果 contextvalue 是一个对象这种复杂结构, 然后存在多个消费者, 每个消费者可能只是订阅一部分 value. 但是由于 context 的设计, 只要 value 部分变了, 那么所有的消费者都会被通知, 那么有很大的可能所有的消费者组件都被重新渲染了.

基于这个问题 Dan Abramov 也是有给出一些解法. 其实最直白的做法就是将多个 context 分离成几个更小的. 不过我在搜索的过程中发现了另外一个似乎更精巧的解法, 虽然可能有点简陋. 但是用在我们项目里我觉得应该够了(其实我不确定, 等过两天 mentor 给我 review 代码的时候再问问)?

context

先看看最基本的使用 context 的做法, 也是评论里 @李引证 提到的做法, 不过这里我略有修改.

// editorContext.jsimport React from 'react'// actions
export const setCursorPosition = () => {// ...
}export const setSelections = () => {// ...
}// context
export const EditorStateContext = React.createContext()
export const EditorDispatchContext = React.createContext()const initialState = {cursorPosition: {lineNumber: 1,column: 1},selections: ['']
}function editorReducer(state, action) {// ... reducer logic
}export function EditorProvider({ children }) {const [state, dispatch] = React.useReducer(editorReducer, initialState)return (<EditorStateContext.Provider value={state}><EditorDispatchContext.Provider value={dispatch}>{children}</EditorDispatchContext.Provider></EditorStateContext.Provider>)
}export function useEditorState() {const context = React.useContext(EditorStateContext)return context
}export function useEditorDispatch() {const context = React.useContext(EditorDispatchContext)return context
}

这里我选择用 useReducer() 也是因为其实我的 editor 状态有点复杂, 而且 cursorPositionselections 是对应两个不同的消费者组件. 如果单纯这么用, 其实是有一点我之前提到的性能问题的

mapStateToProps

参考了 React Context API and avoiding re-renders 这个问题下的一个回答. 其实核心就在于用 React.memo 以及将相关对应的 context 上的 value map 到对应的消费者组件的 props 上, 相关 props 变了, 组件才重新渲染. 虽然感觉兜兜转转绕了半天又绕到了 Redux 上....

// editorContext.js
// ... export const useEditorCursorState = () => {const { cursorPosition } = useEditorState()return {cursorPosition}
}export const useEditorSelectionState = () => {const { selections } = useEditorState()return {selections}
}export function connectToContext(WrappedComponent, select) {return props => {const selectors = select()return <WrappedComponent {...selectors} {...props} />}
}

结合我之前的 EditorStatusBar 一起用:

// App.jsimport Editor from './Editor'
import StatusBar from './StatusBar'export default class App {constructor(props) {super(props) {this.editorRef = React.createRef()}}render() {return (// ... other components<EditorProvider><div><Explorer /><Editor editorRef={this.editorRef}/></div> <StatusBar /></EditorProvider>)}
}// Editor.jsimport React from 'react'
import { useEditorDispatch, setCursorPosition, setSelections
} from './editorContext'
import MonacoEditor from 'some-thirdparty-react-monaco-package'const Editor = React.memo((props) => {const { setCursorPosition, editorRef } = propsconst editorDispatch = useEditorDispatch()const handleEditorDidMount = () => {editorRef.current.onDidChangeCursorPosition(ev => {useEditorDispatch(setCursorPosition(ev.position))})editorRef.current.onDidChangeCursorSelection(ev => {useEditorDispatch(setSelections(ev.selections))})}return (<MonacoEditor editorDidMount={handleEditorDidMount} />)
})export default Editor// StatusBarimport React from 'react'
import Button from '@material-ui/core/Button';
import { connectToContext, useEditorCursorState } from './editorContext'const StatusBar = React.memo((props) => {const { cursorPosition } = propsreturn (<Button size="small">{cursorPosition}</Button>)
})export default connectToContext(StatusBar, useEditorCursorState)

如果你有更优雅的解法, 请一定告诉我...

- END -

editor编辑器为什么头部信息会不见_简单聊一聊一个前端编辑器的性能优化相关推荐

  1. java代码统计收藏量_干货收藏 | 35个Java 代码性能优化总结(上)

    原标题:干货收藏 | 35个Java 代码性能优化总结(上) 前言 代码优化,一个很重要的课题.可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这 ...

  2. spark 算子使用类变量_自己工作中超全spark性能优化总结

    来源:https://zhuanlan.zhihu.com/ p/108454557 作者:一块小蛋糕 编辑:深度传送门 Spark是大数据分析的利器,在工作中用到spark的地方也比较多,这篇总结是 ...

  3. android edittext不可复制_精选Android中高级面试题:性能优化,JNI,设计模式

    性能优化 1.图片的三级缓存中,图片加载到内存中,如果内存快爆了,会发生什么?怎么处理? 参考回答:首先我们要清楚图片的三级缓存是如何的: 如果内存足够时不回收.内存不够时就回收软引用对象 2.内存中 ...

  4. webgl限制帧率_也聊webgl中的大场景性能优化

    随着项目越来越复杂,很多对大场景渲染支持已经成为了"刚需".但是,对于很多经验有限的同学,似乎找不到相关思路.那么,我们就来聊聊,如何进行 webgl 的性能优化. 首先性能优化是 ...

  5. kindeditor编辑器图片上传session丢失_微信公众号排版编辑器全指南!

    好的文章除了内容好之外,排版也是非常重要的,好的排版能够提高读者的阅读体验,让读者在阅读你的文章的时候能将注意力放在内容上,享受阅读. 所以,优秀的公众号运营者即要懂得做内容,也要懂得编辑,以下三个排 ...

  6. eclipse建java项目不见_秒建一个后台管理系统?用这5个开源免费的Java项目就够了...

    这是我的第 196 期分享 作者 | Guide 来源 | JavaGuide(ID:JavaGuide) 分享 | Java中文社群(ID:javacn666) 大家好,我是 Guide 哥,一个三 ...

  7. 7z 头部错误 数据错误_简单的方法来修复损坏的7Zip的文件

    7-Zip是,它允许用户创建不同的Windows系统上的归档文件的软件之一.用几个简单的步骤,不同的压缩级别此应用程序压缩文件.有时7zip的文件遭到损坏,由于多种原因.在情况下,如果有损坏7zip的 ...

  8. websphere不释放游标_不懂别瞎搞!Redis 性能优化的 13 条军规!

    以下文章来源于Java中文社群 ,作者老王 Redis 是基于单线程模型实现的,也就是 Redis 是使用一个线程来处理所有的客户端请求的,尽管 Redis 使用了非阻塞式 IO,并且对各种命令都做了 ...

  9. queueing 优化_简单聊聊网页的资源加载优化

    移动开发中很重要的一块是资源的加载优化.移动开发由于网速低带宽,高延迟,移动设备小内存,低处理器性能的原因,因此很多时候不得不通过优化前端页面的性能来满足用户对网页加载的预期. 前段时间做了相关方面的 ...

最新文章

  1. SpringSecurity学习:1(第一个SpringSecurity项目)
  2. eclipse创建springboot项目_idea创建基于gradle构建的spring boot项目
  3. python平稳性检验_Python数据分析0.3 用statsmodels进行ADF平稳性检验
  4. js控制只能输入数字和小数点
  5. python 重定向 ctf_3.CTF——python利用工具
  6. IDEA连接PostgreSQL数据库
  7. 菜鸟编译OPenJDK全过程记录
  8. vue:axios二次封装,接口统一存放
  9. LINUX系统管理与应用
  10. 让IIS7支持SSI功能(用来支持shtml)的方法
  11. tf.cast()的用法(转)
  12. qt中将按钮指向的鼠标变成手型
  13. 带音效的计算机软件,音效增强软件哪个好用?好用的音效增强软件推荐
  14. gunicorn 安装部署详解
  15. css禅意花园讲了什么——读书笔记1
  16. 电容器和电池有什么不同?
  17. 基于JSP网上书店系统的设计与实现
  18. 首批红米Note9Pro陆续到货,网友分享感受:一条差评让人哭笑不得
  19. 关于分类数据编码所需了解的所有信息(使用Python代码)
  20. 【数据结构与算法】二叉树(下)

热门文章

  1. 系统什么时候会用到swap分区?
  2. 2016-6-16 拓展练习
  3. struts json序列化遇上replaceAll就出问题
  4. AngularJS之过滤器
  5. 【读书笔记】原型模式代码(C++) 第一版
  6. 在机器学习中,ground truth是什么意思?
  7. piccolo2d android,如何在Piccolo2D中打洞?
  8. Dive Into Thrift Node-安装
  9. 临沂经济技术开发区 智慧让城市建设更美好
  10. CISCO交换机配置100例