• 一次WebWorker性能优化经历
  • 发布于 1个月前
  • 65 热度
    0 评论
需求背景
一个浏览器在线代码编辑器中的功能,Cmd+S/Ctrl+S 时屏蔽浏览器的保存,并自动格式化编写的代码。

初步功能实现
先去查询了一下 monaco-editor1文档官网, 查看 monaco 实例 是否提供了格式化代码的 api 。
this.monacoInstance.trigger("editor", "editor.action.formatDocument");
这么一看需求还是蛮简单的。
监听 monaco的 onKeyDown 事件:
onKeyDown={(event) => {
  const keyCode = event.keyCode || event.which || event.charCode;
  const isCtrlOrCmdPressed = event.ctrlKey || event.metaKey;
  if (keyCode === 83 && isCtrlOrCmdPressed) {
    this.monacoInstance.trigger("editor", "editor.action.formatDocument");
    event.preventDefault();
  }
}}
然后本地测试确实可以格式化代码成功,但是当我将编辑器中代码量增加到1000行以上,故意把格式化错乱,再次点击 Cmd+S/Control+S 保存时,会有卡顿效果。是因为在格式化过程算法实现会进行复杂的语法分析和规则应用,对于大量的代码,进行语法分析和根据格式化规则进行处理可能需要大量的计算资源和时间。需要遍历整个代码树,检查各种语法结构,应用缩进规则、换行规则等,阻塞主进程,那基于这种卡顿场景有什么优化办法呢?

可以想到的优化应该如下几点:
1.优化格式化算法
检查 editor.action.formatDocument 所使用的格式化算法,看是否有可优化的空间。例如,减少不必要的重复计算或优化数据结构的使用。
2.异步处理
将格式化操作放在一个异步任务中进行,避免阻塞主线程。这样,用户在保存时可以继续进行其他操作,而格式化在后台进行。
3.代码分段处理
不是一次性对整个大量的代码进行格式化,而是将代码分成较小的段,逐步进行格式化,以减少一次性的处理量。
4.缓存和复用
对于已经格式化过的代码段,如果没有修改,可以缓存其格式化后的结果,下次直接使用,避免重复计算。

方案1 3 4改动和成本都比较高,选择了方案 2(注意 WebWorker 只是异步任务中的一种方式),开启 WebWorker 线程是实现异步格式化操作

使用 WebWorker 优化
WebWorker前置基础知识回顾
使用 WebWorker之前,先说几个 WebWoker 基础知识点:
1.同源限制:worker 线程执行的脚本文件必须和主线程脚本文件同源
2.限制访问:worker 不能访问 DOM,也不能直接操作主线程的变量和函数,worker 线程和主线程 window 不同的另一个全局上下文中运行,无法读取主线程所在网页的 DOM 对象并操作,也不能获取document,window 等对象,但是可以获取 navigator,location,XMLHttpRequest,setTimeout 等。注意 worker 线程中可以使用 fetch
3.消息传递:主线程和 Web Worker 通过 postMessage 方法传递消息,通过 onmessage 事件接收消息。这里特别说一下结构化克隆算法:postMessage 时,数据通过结构化克隆算法传递。这意味着大多数原生JavaScript 对象可以被传递,但是函数 和 DOM元素不可以。

Webpack 配置
webpack 5中的worker路径加载
因为目前项目中使用的 Webpack5, 从 Webpack52开始,可以使用 WebWorkers 代替 worker-loader(也就是不再需要在 rules 中配置 worker-loader 了)。
语法:
new Worker(new URL('./format.worker.js'),import.meta.url)
webpack配置globalObject支持webWorker
在 webpack 配置中不要忘记修改 output 中 globalObject为 self
output: { globalObject: 'self', // 用于支持 web workers filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), },
主线程与worker代码编写
因为 Worker的限制,在 worker 中是不可以操作 Dom元素的,并且也无法获取主进程中的变量,比如主进程中的 monacoInstance。所以这种方式this.monacoInstance.trigger("editor", "editor.action.formatDocument"); 在 worker中进行格式化不可以(尽管把 monacoInstance 是可以传递进去,但是操作 Dom元素也是不可能的!!!)

所有换了一个思路,在 worker 中格式化当前主进程(编辑器)传递过来的代码,然后 worker 中格式化完成后,再将代码传递出去,更新到 monaco组件编辑器中,这样因为算法计算导致的主进程卡顿问题也解决了。

创建 Web Worker 文件

1.创建一个新的 JavaScript 文件,test.worker.js,编写格式化操作

worker 中的格式化操作不再使用 monaco 的 api,改为使用 prettier/standalone 和  prettier/parser-babel 模块。可扩展性更强了,可配置格式化内容。代码如下:

import * as prettier from 'prettier/standalone';
import * as parserBabel from 'prettier/parser-babel';

self.onmessage = function(event) {
  const { code, parser } = event.data;
  debugger
  const formattedCode = prettier.format(code, {
    parser: parser,
    plugins: [parserBabel]
  });
  self.postMessage(formattedCode);
};
2.在主页面中创建和使用 Web Worker
这部分代码只包含了worker相关部分,monaco组件的封装代码未附上因与本文无关
class Editor extends React.Component<IEditorProps,IEditorState>{
    private formatterWorker;
    constructor(props: IEditorProps) {
      super(props);
      this.formatterWorker = new Worker(new URL('./formatter.worker.js',import.meta.url));
      this.formatterWorker.onmessage = this.handleFormatterWorkerMessage.bind(this);
  }
  // 省略组件封装相关代码
  
   handleFormatterWorkerMessage(event){
    const formattedCode = event.data;
    this.monacoInstance.setValue(formattedCode);
  }
  
  render() {
    const { className, style, options } = this.props;
    // 省略部分代码
    let renderStyle = {
      width: '100%',
      height: '100%',
      overflow: 'auto',
      minWidth: '1200px',
      minHeight: '280px',
    };
    renderStyle = style ? Object.assign(renderStyle, style) : renderStyle;
    return (
      <section
      onKeyDown={(event) => {
        const keyCode = event.keyCode || event.which || event.charCode;
        const isCtrlOrCmdPressed = event.ctrlKey || event.metaKey;
        if (keyCode === 83 && isCtrlOrCmdPressed) {
          const code = this.monacoInstance.getValue();
          this.formatterWorker.postMessage({ code: code, parser: 'babel' });
        }
      }}
        style={renderStyle}
        ref={(domIns: any) => {
          this.monacoDom = domIns;
        }}
      />
    );
  }
}
注意:WebWorker 创建 this.formatterWorker = new Worker(new URL('./formatter.worker.js',import.meta.url));
注意:如果你使用的 TypeScript 版本支持 import.meta.url ,你需调整 tsconfig.json 中的模式为 ES 模式方式,确保module设置为es2020或更高。

总结
至此,使用 WebWorer 来优化 Monaco 代码编辑器实现自动格式化功能已实现,欢迎大家交流。既然已经使用了 WebWorker 在项目中,那就优雅一点。下一篇文章预告,如何封装一个优雅的 WebWorker

用户评论