Web 框架从运行库到优化编译器:JIT 与 WebAssembly 性能原理精读 文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载本文是「前端精读周刊」we/weekly对 Tom Dale 经典文章《Compilers are the New Frameworks》的深度解读先从“Web 框架正在从运行库转变为优化编译器”这一核心观点切入分析 PriJs、UmiJs 一类“约定优先、编译内置”的一站式框架为何能减少前端工程化负担再以 JIT 的监视器、基线器、优化器三层机制为主线逐项对比 WebAssembly 在 Parse、Compile、Re-optimize、Execute、垃圾回收五个阶段相对 JS 的性能优势最后结合本仓库编译原理、V8 引擎等系列文章给出前端开发者学习编译器原理的实践路径。读完你将理解“框架即编译器”的架构趋势并掌握 JIT 与 WebAssembly 性能差异的底层依据。1 引言Web 框架正在从运行库转变为优化编译器本期精读文章《Compilers are the New Frameworks》篇幅短小却言简意赅作者在开篇就抛出核心观点Web 框架正在从运行库runtime library转变为优化编译器optimizing compiler。作者主要从编译性能入手展开论述同时指出WebAssembly 可能将会是下一代 Web 应用的落脚点因此建议 Web 开发者深入了解、学习编译器的工作原理。这一判断在今天的生态中已经被大量印证无论是以“编译时优化”为核心卖点的框架还是被浏览器内置进 JS 引擎的 JIT 编译器其本质都是把“运行时的性能负担”前移到“编译期解决”。本仓库中与之呼应的内容非常丰富既有《手写 SQL 编译器 - 词法分析》 系列讲述前端如何亲手实现一个编译器也有《V8 引擎 Lazy Parsing》、《JS 引擎基础之 Shapes and Inline Caches》 等文章剖析浏览器引擎内部的编译优化还有《用 Babel 创造自定义 JS 语法》 演示如何在词法、语法层面扩展一门语言。这些内容共同构成了理解“框架 编译器”这一论断的完整知识链。2 概述前端工程的复杂度倒逼框架转型目前业界流行使用一整套工具来搭建前端项目例如 webpack、webpack-dev-server、babel、scss、react、redux、react-router 等。在项目开发期间开发者需要花费大量时间进行工程性能优化、编写大量构建配置项。从前端工程的复杂度以及前端开发的工作量来看前端框架已经不能再仅仅只是一个单独的视图层或数据处理层而应该是一套相对完整的框架它不仅要提供“如何编写前端页面”的方法同时也应该考虑代码构建编译的性能、页面间路由的跳转、新语法的兼容等一系列问题。这正是本期精读文章抛出的观点Web 框架正在从运行库转变为优化编译器——或者说Web 框架需要将“优化编译性能”纳入自身设计的核心考量而不是把它留给使用者去手动配置。2.1 从运行库到一站式框架PriJs UmiJsPriJs 与 UmiJs 正是以上述观点为基础的代表二者都是基于 react、并包含了工具、路由、性能优化、数据流等能力的强约定弱配置的前端一站式框架。它们通过约定、自动生成和解析代码等方式来辅助开发减少开发者在性能、配置、路由、构建上耗费的时间让开发者可以更专注于业务逻辑。这类框架的核心价值在于把“编译与构建”从开发者的日常工作里抽离出来封装成框架的默认能力——这恰恰就是“把框架当作优化编译器”的直接体现。构建工具webpack 是目前主流的前端代码构建工具但其复杂的配置一直是前端开发者头疼之处。PriJs UmiJs 框架内部解决了这一难题它们将 webpack 复杂的性能优化配置全部内置化使项目在0 配置的基础上直接支持能力说明PWA渐进式 Web 应用能力开箱即用Automatic code splitting自动代码分割Tree Shaking摇树优化剔除未使用代码Auto dll自动抽取公共依赖为 dllImport on demand按需加载Auto pick shared modules自动挑选共享模块Scope Hoist作用域提升Dynamic import动态导入Service Worker离线缓存能力Sass LoaderSass 样式编译内置可以看到原本需要开发者手工逐项配置的优化项在“框架即编译器”的思路下全部被内置化开发者拿到的是一个开箱即用的编译管线。这与本仓库中《webpack4.0 升级指南》 讨论的构建工具演进方向是一致的构建配置正在从“开发者自治”走向“框架托管”。页面与路由PriJs UmiJs 提供页面生成模板并自动根据项目页面生成路由通过单页面或多页面特性决定路由跳转的类型默认提供 404 页面。也就是说路由不再由开发者手写配置而是由框架在“编译期”根据目录结构自动推导生成——这是典型的“约定优于配置”模式也再一次说明框架承担了大量编译期的推导工作。数据流PriJs UmiJs 虽然是基于 react 的前端一站式框架暂不支持 vue、angular 等但并不局限数据流的使用方式开发者可以根据项目需求使用任意数据流方案如 redux、mobx 等。这保证了“编译能力集中、业务层开放”的平衡。插件机制PriJs UmiJs 提供了灵活的插件机制使项目能够拥有强大的定制能力。通过插件机制可以变更 webpack 配置修改路由规则修改页面模板新增命令使用任意数据流定制项目规范和约定。插件机制保证了框架在“强约定”之外仍然保留“可扩展”的空间避免框架固化为无法定制的黑盒。其它能力此外PriJs 还支持 markdown 格式、支持 Deploy to github pages、支持 Typescript 等。PriJs UmiJs 这类前端一站式框架实际上提供了一整套前端开发解决方案它不仅仅只是一个单纯运行库而是将构建性能、工具、路由等一系列问题全部解决。这种做法在一定程度上正是对 “Compilers are the New Frameworks” 这一观点的实践佐证。3 精读从 JIT 到 WebAssembly精读文章作者建议 Web 开发者学习编译器工作原理。对前端开发者来说可以从与前端现在和未来息息相关的JIT和WebAssembly入手学习编译器相关原理。3.1 JIT为解释型语言引入编译器JITJust-in-Time即时编译主要是针对 javascript 这一解释型语言所做的性能优化即浏览器引入编译器来解决解释器性能低效的问题形成“解释器 编译器”的混合模式。简单来说纯解释执行速度慢但启动快纯编译执行速度快但启动慢。JIT 的混合策略是代码先快速解释执行在执行过程中收集运行信息把高频执行的代码段动态编译为机器码从而兼顾启动速度与执行效率。本仓库《JS 引擎基础之 Shapes and Inline Caches》 对此有更细致的描述V8 将解释器称为 Ignition点火器将优化编译器称为 TurboFan涡轮风扇发动机代码先“点火启动”快速解析成可执行的字节码再在执行过程中利用获取的数据如执行频率将高频方法编译为机器码以提速。JIT 的整体流程可拆解为监视器、基线器、优化器三个角色。监视器Monitor浏览器在 JS 引擎中增加一个监视器用于监控通过解释器的代码的运行情况并将同一行代码运行若干次标记为warm温代码将同一行代码运行很多次标记为hot热代码。这里“warm / hot”的判定本质上是频率统计运行次数决定了代码段会被推进到哪一级编译管线这是 JIT 最重要的输入信号。基线器Baseline CompilerJIT 会将warm代码段放到基线编译器中并将编译结果存储起来。该代码段的每一行都会被编译成一个stub桩并以行号 变量类型为索引。如果监视器监视到了执行同样的代码和变量类型就直接将对应的已编译版本提交给浏览器执行而不用重新通过解释器来翻译通过这样的做法可以加快执行速度。这一“以行号 变量类型为索引”的设计正是《JS 引擎基础之 Shapes and Inline Caches》 中 Inline Cache局部缓存思想的体现引擎把查找结果缓存到指令中下次命中相同 Shape 时直接跳过查找过程。优化器Optimizing CompilerJIT 会将hot代码段放到优化编译器中进行代码优化不过需要遵循优化规则即如果代码循环中每次迭代的对象都有相同的形状shape那么就认为它以后迭代的对象的形状也是相同的。但 javascript 是没有类型定义的无法确保每次代码迭代的对象都会具有相同类型因此在代码运行前会检查其规则是否合理如果合理则执行优化代码如果不合理则丢弃优化代码重新回到解释器或基线器。大多数浏览器为了防止引起“优化 - 丢弃优化”的无限循环一般会对优化次数做限制比如 JIT 做了超过10 次“优化 - 丢弃优化”的操作就不再执行优化编译。本仓库《V8 引擎特性带来的的 JS 性能变化》 中提到的“某些代码组合情况下陷入无限优化循环”的安全隐患正是优化器这一机制的边界情况。JIT 的额外开销JIT 在优化提升 javascript 性能的同时也会增加多余的其它开销主要是对代码的监视和编译时间的开销具体包括优化和丢弃优化的开销监视器存储的内存开销丢弃优化时恢复存储的内存开销基线版本和优化后版本的内存开销。而这些多余开销正是 WebAssembly 试图从更底层去解决的部分。3.2 WebAssembly为编译器而生的更底层方案为什么说 WebAssembly 更为高效、性能更好关键在于理解 JS 在 JS 引擎中的性能消耗分布。在 JS 引擎中性能消耗的分布大致为将源码转为解释器可运行代码 → 基线 优化编译器的运行 → 优化-丢弃优化的过程 → 执行代码 → 垃圾回收 内存清理这个过程是交叉进行的代码一边执行一边被监视、编译、优化或丢弃优化任意时刻都可能发生阶段切换因此整体流程较长且开销分散。而 WebAssembly 却只要简单的三个步骤即可完成 JS 引擎的整个交叉执行过程因为它的设计目标就是跳过 JS 那些“动态类型带来的反复试探”直接以静态、确定的形态进入引擎。Parse解析当到达浏览器时JS 源码需要被解析成 AST抽象语法树再变成字节码提供给引擎编译而 WebAssembly不需要这种转换因为它本身就是字节码只需对代码进行decode解码并检查其正确性即可。这与本仓库《手写 SQL 编译器 - 词法分析》 中描述的“字符串 → Token → AST”过程形成鲜明对比凡是基于文本的语言都绕不开字符解析这一步而 WebAssembly 从诞生起就避开了文本形态。Compile Optimize编译与优化这是执行代码编译和优化的阶段。在这个阶段 WebAssembly 的性能优于 JS主要原因有三点WebAssembly是有类型定义的代码不需要在编译前运行代码来获取变量类型WebAssembly 不需要像 JS 那样当变量类型改变时需要将代码编译成不同版本WebAssembly 不需要在编译阶段做太多的优化工作。换句话说JS 的优化编译器把大量精力消耗在“类型推断与版本切换”上而 WebAssembly 的类型在编译期就已确定编译器可以直奔主题。Re-optimize重优化当 JIT 在执行 JS 阶段发现变量类型不合理就会丢弃优化代码重新进行“优化 - 丢弃优化”的循环。而 WebAssembly 中的变量类型都是确定的JIT 不需要检查变量类型的合理性因此并没有重优化阶段。“没有重优化阶段”意味着JIT 为 JS 维护的那套 10 次上限的降级循环、基线版本与优化版本并存的内存开销在 WebAssembly 身上全部不存在。Execute执行如果开发者了解 JIT 的内部实现机制当然可以针对性地写出符合 JIT 标准的代码使其具有更高的执行效率。但通常开发者为了代码可读性更好而使用的编码模式往往却不适合编译器对代码的优化而且不同浏览器的优化规则也不尽相同导致 JS 的执行效率并不高。WebAssembly 正是为了编译器而设计的很多 JIT 为 JS 所做的优化WebAssembly 并不需要这使得 WebAssembly 能够专注于提供执行效率更高的指令。Garbage collection垃圾回收JS 不支持开发者手动清理内存而是由 JS 引擎自动做垃圾回收因此垃圾回收的时机并不可控有可能会在一个不合适的时机执行而且也会增加代码执行的开销。而对于 WebAssembly 而言其内存操作是由开发者手动控制的虽然会增加一些开发成本但这使得代码执行效率更高——开发者可以在最合适的时间点分配与释放内存避免 GC 停顿打断计算密集任务。4 延伸前端开发者如何入门编译器原文章作者建议 Web 开发者深入学习编译器工作原理。结合本仓库已有的系列内容可以梳理出一条清晰的学习路径。4.1 先建立编译器的整体流程认知《手写 SQL 编译器》系列 是一个面向前端的完整编译器入门解析 SQL 分为四步——词法分析将字符串拆分为 Token、语法分析将 Token 解析为 AST、错误检测与提示推断、语义分析。其中词法分析阶段的“头匹配正则 循环切分”范式以及方言拓展如支持${variable}、中文字段的思路能帮助读者理解任何编译器包括 JIT 背后的 parser的起点。《用 Babel 创造自定义 JS 语法》 则展示了完整闭环从注册Token、修改词法分析器到在语法分析阶段为函数节点添加curry属性再到编写 babel 插件实现柯里化转换。这篇文章可以让读者直观看到“词法 → 语法 → AST → 插件转换”整条链路如何运转也能理解为何框架层面会把“解析与转换”当作核心竞争力。4.2 理解浏览器引擎的编译优化惰性解析Lazy Parsing《V8 引擎 Lazy Parsing》 说明 V8 通过 preparser 跳过不必要函数的编译只在调用时才全量解析以节省 CPU、内存与磁盘占用同时解析了模块化打包产生的 IIFE 闭包对惰性编译的干扰。Shapes 与 Inline Caches《JS 引擎基础之 Shapes and Inline Caches》 解释了 JIT 优化器赖以工作的“对象形状”概念以及引擎如何通过局部缓存加速属性访问——这正是 JIT 优化规则“相同形状假设”得以成立的基础。引擎层面的性能变化《V8 引擎特性带来的的 JS 性能变化》 从工程视角列举了 try/catch、delete、arguments、bind、多态函数等写法的性能现状帮助开发者在日常编码中写出更符合 JIT 优化预期的代码。5 总结本文从Web 框架正在从运行库转变为优化编译器这一观点切入讨论了 PriJs UmiJs 前端框架的思路转变构建内置化、路由自动生成、插件机制、数据流开放并简洁描述了 JIT 的工作原理监视器、基线器、优化器与优化-丢弃优化循环以及 WebAssembly 相比于 JS 在 Parse、Compile Optimize、Re-optimize、Execute、Garbage collection 五个阶段的性能优势。本文的主要目的是希望读者积极参与讨论这一观点因此并未长篇剖析 JIT 与 WebAssembly 的深刻原理。但深入学习编译器的工作原理对 Web 开发者来说绝对是受益匪浅的事情——本仓库的编译原理模块词法分析、语法分析、回溯、语法树、错误提示、缓存优化、智能提示与V8 引擎系列正是面向前端开发者的一条完整编译器学习路径可以作为后续深入研读的起点。赞分享文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐Dart SDK 编译器架构与多平台运行从 JIT/AOT 原生运行时到 JavaScript 与 WebAssemblyDart SDK 编译器架构与多平台运行从 JIT/AOT 原生运行时到 JavaScript 与 WebAssembly 本篇文章以 Dart SDK 官方编程语言编译器语言运行时标准库开发工具Enso运行时架构高性能JIT编译器解析Enso运行时架构高性能JIT编译器解析 Enso运行时架构是一个高度集成化的系统构建在GraalVM之上采用了现代化的语言实现技术栈。其核心设计理念是将数据工程数据分析后端Stable Video Infinity扩展功能SVI-Talk与SVI-Dance模块使用教程Stable Video Infinity扩展功能SVI Talk与SVI Dance模块使用教程 Stable Video InfinitySVI作为I人工智能大模型媒体生成深度学习计算机视觉微调上一篇es-toolkit uniqBy 完全指南基于映射键的数组去重实现与源码解析下一篇miniblink49 内置 Google Mock 实战 FAQMatcher 迁移、模板错误诊断与常见陷阱全解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考