likes
comments
collection
share

前端开发者必不可少的AOT和JIT知识

作者站长头像
站长
· 阅读数 30

一、前言

编译 现在已经是我们开发过程中必不可缺少的流程了,做为前端开发者我们接触最多的编译器就是 babel 了,在编译的过程中我们可以做下面这些事:

  • 将框架中描述的 UI 转换成宿主环境可以识别的代码,比如在 sevlet 框架中可以直接将描述 UI 的模版字符串转换成命令式的 js 代码,在 React 框架中可以将 Html 编译成 一个个 React.createElement() 函数。
  • 对代码进行转化,比如将 Typescript 转换成 Javascript、将 es6 转换成 es5、对一些新特性进行 polyfill、对 less/sass 进行预处理等。
  • 对代码进行优化,比如代码的压缩、Treeshaking等。

编译 的过程可以放在两个时机去执行,一个是 AOT(构建时编译/预编译),另一个就是 JIT(即时编译),也就是说在代码执行的时候去编译,下面主要会介绍这两种编译方式的区别以及在前端框架中的应用。

二、AOT 和 JIT

使用过 Angular@1Vue@1 的同学可能就知道,我们通常会把模版写在 javascript 里面,此时这个模版就是一个字符串,这个字符串最终会被编译为 javascript 代码,默认情况下这个编译的过程是在浏览器中进行的,即 JIT,那么这就会带来一个问题,因为编译是需要时间的,此时我们消耗的就是用户的时间,既然这样那我们为什么不放在构建时来做呢,这就有了 AOT。

因为 Angular 同时提供这两种编译方案,下面我们将会用它来介绍这两种编译方式。

1、AOT

AOT 即在构建期间去编译我们的代码,在浏览器中我们可以直接下载并运行编译后的代码,从 Angular 9 开始编译默认设置为 AOT,下面是使用 AOT 后项目的构建流程:

前端开发者必不可少的AOT和JIT知识

那采用这种编译方式有什么优点呢?

  • 更快的渲染速度

    使用 AOT 后浏览器会下载应用程序编译后的代码并直接执行,而无需等待先编译应用程序。

  • 更小的文件体积

    因为代码已经是编译后的代码,所以在最终的产物中就不需要包含 Angular 编译器,因此他会大大减少应用的负载。

  • 更早的检测代码中的错误

    考虑下面 Angular 代码:

    @Component({
      selector"root",
      template"<h3>{{getName()}}</h3>"
    })
    export class AppComponent {
      public getName() {
        return 'xl';
      }
    }
    

    最终页面将会正常渲染出 xl ,假如我将 h3 标签中的 getName 改为 getAge:

    - template"<h3>{{getName()}}</h3>"
    
    + template"<h3>{{getAge()}}</h3>"
    

    如果使用 AOT,编译后会立即报错:

前端开发者必不可少的AOT和JIT知识

  • 更好的安全性

    AOT 在将 HTML 模板和组件提供给客户端之前就将其编译为 JavaScript 文件,由于没有要阅读的模板,也没有危险的客户端 HTML 或 JavaScript 评估。

  • 更少的异步请求

    编译器在 Javascript 中内联外部 HTML 模版和 CSS 样式,所以我们不需要单独发送 ajax 请求去请求这些文件。

2、JIT

JIT(即时编译) 即在运行时去编译代码,每个文件都是单独编译的,当我们更该代码时不需要再次构建整个项目,下面是该模式下的流程:

前端开发者必不可少的AOT和JIT知识

那使用 JIT 有什么优势呢?

  • 易于开发调试

    在 JIT 模式下可以生成映射文件,这样便于功能的实现和调试。

  • 编译时间短

    因为大多数编译是在浏览器端完成的,因此编译时间会更少,因此对于一些大项目来说如果某些组件大部分时间不使用,那么此时使用 JIT 是最合适的。

  • 存储热点代码

    如果某个方法或者代码块执行的特别频繁,那么就会被视为 热点代码,然后 JIT 就会对这部分代码进行编译并存储起来,下次使用时就可以直接从内存中读取。

三、AOT在前端框架中的应用

对于框架而言它主要是帮我们做了两件事:

  • 根据组件状态的变化找到变化的 UI,我们听的比较多的 diff 算法就是做这件事的。
  • 将变化 UI 渲染到宿主环境的真实 UI。

而借助 AOT,我们可以对模版语法进行预编译,,这样就能减少第一步的开销,这也是很多框架选择模版语法的一个很重要的原因(vue、Angular、Svelte),因为模版语法的写法是固定的,固定意味着 可分析,而 可分析 就意味着在编译时可以标记模版语法中的 静态部分 动态部分,这样在寻找需要变化的 UI 时就可以跳过这些 静态部分

是不是感觉这已经优化到极致了?还有更厉害的,我们知道 vue 是一个编译时 + 运行时的一个框架,当状态发生改变时他需要通过 diff 算法找到变化的 UI 然后将更新后的 UI 渲染到页面上,也就是说 vue 其实还是需要依靠 diff 算法。

但是对于 svelte/Solid.js 来说他根本不需要用到 diff 算法,他们直接利用 AOT 在编译时建立了 组件状态动态UI的关系,在运行时如果组件状态发生改变就可以直接跳过寻找的过程,直接将更新后的 UI 渲染到页面上,下面我们拿 svelte 举例:

这里是一段 svelet 代码:

<h1>Hello {{name}}</h1>

下面只展示编译后渲染和更新部分的代码:

// 渲染部分
h1 = createElement('h1')
text = createText(
    text_value = state.msg
);
insertNode(h1, target, anchor);
appendNode(text, h1);

// 更新部分
if (text_value !== (text_value = state.msg)) {
    // 直接更新text节点的值
    text.data = text_value;
}

我们看到在渲染部分都是一些比较命令式的代码,通过原生的 API 去创建节点/文本,而在更新部分我们看到,当我们更新 name 值的时候,他会直接将最新的值赋值给这个文本节点,这样不仅可以减少产物的体积,同时也能获得很好的性能。

四、AOT在JSX中的应用

上面介绍了 AOT 在模版语法中应用,他能有效的减少在 diff 中的时间消耗,那在 JSX 语法中效果怎么样呢?其实采用 JSX 描述 UI 的前端框架很难从 AOT 中受益,因为 JSX 太灵活了,他只有在执行之后才能知道结果,所以很难去做静态分析。

所以要想让使用 JSX 的前端框架在 AOT 中受益只有两个思路:

  • 使用新的 AOT 思路
  • 约束 JSX 的灵活性

对于第一种优化思路 React 是尝试过的,facebook 团队曾经推出一款 React 编译器 prepack 用来实现 AOT 优化,他的思路是 在保证运行结果一致的情况下,改变源代码的运行逻辑,输出性能更高的代码,下面看一个例子:

(function () {
  function fibonacci(x) {
    return x <= 1 ? x : fibonacci(x - 1) + fibonacci(x - 2);
  }
  global.x = fibonacci(10);
})();

在这个自执行函数中定义了一个斐波那契函数,最终将第十个值赋值给 global.x,下面看一下编译后的结果:

(function () {
  var _$0 = this;

  _$0.x = 55;
}).call(this);

我们看到他直接将 55 赋值给了 global.x,也就是说在编译的时候 prepack 是会真正的执行一遍你的代码,如果这个值是一个定值他就会直接把计算后的结果赋值给你同时删除多余的代码(试一试)。

其实 React 还有一些其他的优化思路,比如 babel-react-optimize 这个插件,他会将 React 中的一些静态节点直接提取出来作为一个常量保存起来,这样在进行比对的时候发现前后两个值的引用是一样的就可以直接跳过,比如下面这个例子:

class MyComponent extends React.Component {
  render() {
    return (
      <div className={this.props.className}>
        <span>Hello World</span>
      </div>
    );
  }
}

编译后的结果如下:

var _ref = <span>Hello World</span>;

class MyComponent extends React.Component {
  render() {
    return (
      <div className={this.props.className}>
        {_ref}
      </div>
    );
  }
}

同样使用 JSX 的还有 Solid.js ,它采用的是第二种优化思路,它实现了几个内置的组件用于描述 UI 的逻辑,从而减少了 JSX 的灵活性,比如下面几个组件:

  • 用 For 替换数组的 map 方法
<For each={state.list} fallback={<div>Loading...</div>}> 
    {(item) => <div>{item}</div>} 
</For>
  • 用 Show 组件替换条件判断语句
<Show when={state.count > 0} fallback={<div>Loading...</div>}>
    <div>My Content</div> 
</Show>
  • 用 Switch 和 Match 替换 switch-case 语句
<Switch fallback={<div>Not Found</div>}>
    <Match when={state.route === "home"}> 
        <Home />
    </Match> 
    <Match when={state.route === "settings"}> 
        <Settings /> 
    </Match> 
</Switch

五、总结

本片文章主要讲述了 AOT 在前端框架中的应用,在 AOT 阶段可以对代码进行静态分析,从而减少 状态变化 --> 找到变化的UI 这一步的工作量,但是在框架中使用 AOT 的前提是代码是可以分析的,像 Vue、Angular 都使用了模版语法,在编译阶段就能分析出哪些是 静态节点,而对于像 React 这种使用 JSX 的框架来说因为本身语法过于灵活导致许多组代码需要等到实际运行才能知道结果,自然也就不太适合使用 AOT 进行优化,只能另辟蹊径。