人物采访 / T SALON

编辑内容

于航谈 WebAssembly 与前端成长:从运行原理到长期积累

回顾 2022 年 T Chat 的两段录播:从 WebAssembly 的运行原理、语言生态、生产案例与提案进展,写到于航的技术经历、写书与表达、团队协作、职业选择,以及学习、架构能力和成长机会。

于航在 T Chat 分享 WebAssembly 与前端成长的访谈封面
于航在 T Chat 分享 WebAssembly 与前端成长的访谈封面

本期视频发布于 2022 年。文中的技术状态、个人经历与观点均为当时情况。

这场 T Chat 先讨论技术,再回到做技术的人。第一段是于航的 WebAssembly 年度分享,从 JavaScript 的执行过程讲到模块、工具链、应用案例与生态进展;第二段通过主持人提问,展开他进入前端行业、写书、练习表达,以及对工作环境和长期成长的看法。

当期自我介绍页写明,于航(Jason Yu)在 PayPal 从事软件开发,主要关注前端,也研究 WebAssembly、C/C++、汇编和系统编程。他是《深入浅出 WebAssembly》的作者,曾开设《WebAssembly 入门课》等专栏。

技术分享:WebAssembly 解决了哪一层问题

从 JavaScript 的执行过程说起

于航先用 JavaScript 引擎的执行流程解释背景:源代码需要经过解析,形成相应的内部表示,再由解释执行、运行时信息收集和优化编译等环节配合完成运行。引擎会关注频繁执行的代码,并在一定假设下生成更适合执行的机器码;如果假设失效,执行路径可能需要回退。

他把这条路径拆开讲给前端开发者听:开发者交给引擎的是源代码,引擎先解析代码,再让解释器参与执行。运行一段时间以后,分析器会收集哪些函数或路径被频繁使用、执行时出现过什么类型等信息。优化编译器再利用这些观察结果,针对热代码生成机器码。这里的“优化”不是在写完代码时一次完成的;它依赖程序运行时观察到的条件。

他用 x + y 说明动态类型带来的处理工作。同一个加号,遇到数字、字符串或对象时,涉及的行为并不相同;引擎要在执行过程中处理类型及转换规则。循环中某个变量的类型发生变化,也可能影响此前的优化假设。分享由此引出的问题是:如果程序的一些类型信息能够提前确定,执行准备过程能否有所不同?

为解释回退,他设想一个运行很多次的循环。前面数千次迭代里,x 一直是整数,引擎据此选择了较快的执行路径;后来代码把 x 换成对象,原有假设不再成立,就不能继续照旧运行。这个例子把“动态类型”和“去优化”联系起来:不是 JavaScript 永远不能优化,而是引擎必须面对运行中条件变化的可能。

这是一种理解技术差异的切入方式。它不能单独证明 WebAssembly 在所有应用中都比 JavaScript 快,具体结果仍取决于工作负载、调用方式与运行环境。

抽象指令、模块文件与宿主环境

接下来,于航从几个层面解释 WebAssembly。它包含面向抽象机器的指令,可以作为不同语言的编译目标;这些指令及模块信息通常编码在 .wasm 二进制文件中,文件头也包含用于识别格式的信息。它不对应某一种固定的物理处理器,最终执行仍需要运行环境处理。

他用 x86、Arm 的指令作类比,帮助听众理解“指令集”这个词,但随即强调 Wasm 的抽象性:同一份模块并不是直接交给某颗芯片执行,而是由具体运行时接收、验证并转换成所在设备可以运行的形式。开发者如果用十六进制工具查看文件,会看到标识格式的文件头;这只是识别模块的一步,模块内部还有类型、函数、导入导出等信息。

在浏览器里,配套的 WebAssembly API 负责加载、实例化和调用模块。与从 JavaScript 源码开始的流程相比,Wasm 模块已经具有明确的低层表示和类型信息;运行环境仍需对模块进行解码、验证,并通过编译或解释等方式执行,不能把分享中的流程示意理解成二进制文件无需处理就能直接运行。

于航在演示流程图时,着重比较了两种输入交给引擎之前的状态:JavaScript 输入是需要理解语法和运行行为的源码;Wasm 输入已经是为抽象机器准备的模块。因此,后者省去的是一部分源语言处理工作,不是整个运行时。模块能否带来性能收益,仍要看计算任务本身、JavaScript 与模块之间的调用和数据转换成本,以及宿主环境怎样实现它。

这里也建立了一个贯穿后续案例的区分:WebAssembly 模块与运行它的宿主是两个层面。浏览器可以是宿主,浏览器之外的运行时也可以是宿主;模块怎样接入外部能力,要结合宿主提供的接口理解。

一个加法函数怎样进入浏览器

为了让模块结构可见,于航展示了 WebAssembly 的文本表示 WAT。示例声明一个接收两个整数参数、返回整数的函数,取出参数后执行加法,再以 add 的名字导出。参数类型、函数体和导出关系都在这个小例子里出现了。

他按文本中的顺序逐项解释:param 写明输入,result 写明返回值;函数体里取出两个局部参数,再用 i32.add 相加。讲到 export 时,他没有把它当成无关紧要的收尾语法,而是指出浏览器要从模块外调用这个函数,就需要有一个对外可识别的名字。一个只有几行的加法例子,恰好能把函数签名、内部计算与对外接口三件事放在同一张图上。

实际开发通常不会靠手写这些指令完成复杂业务。分享随后改用 C++ 编写同样的加法函数,通过 Emscripten 构建模块。演示中的 EMSCRIPTEN_KEEPALIVE 用于保留需要导出的函数,extern "C" 则与 C++ 的符号命名问题有关。它们帮助解释了为什么一个普通函数在跨越编译与调用边界时,还需要额外处理。

于航说明,如果程序里没有地方使用这个函数,编译器可能把它视为无用代码并移除。EMSCRIPTEN_KEEPALIVE 是为了让构建产物中仍能找到它;extern "C" 则让外部调用方能够按预期的名字找到函数,而不必猜测 C++ 编译器改写后的符号名。他展示的命令还包含指定输出文件和优化等级的参数。这里最值得注意的不是记住一条命令,而是写好的 C++ 函数如何成为一个能被浏览器找到的导出项。

浏览器侧的过程分为三步:获取模块的二进制数据,调用 WebAssembly.instantiate 实例化,再通过导出对象调用 add。这个例子把编写函数、构建模块和使用模块连在了一起。

他在页面里传入 10 和 20,拿到返回值 30,用最小的运行结果完成闭环。这个操作并不意味着 C++ 里的每一种能力都能原样搬进浏览器;下一段讨论的胶水代码,正是为了解释为什么简单加法之外,还要考虑宿主提供了什么。

Emscripten 不只负责生成一个文件

在展示编译工具时,于航也解释了 JavaScript 胶水代码的作用。C/C++ 程序可能依赖文件读写、标准库或图形接口,而浏览器并不会自动具备与本地程序相同的运行环境。工具链除了生成 Wasm,还需要帮助连接这些依赖。

他用文件操作举例:程序调用的文件接口,可以通过浏览器中的虚拟文件系统等机制实现;某些图形能力也可以由浏览器接口承接。因此,移植工作的关键不只是把语言编译过去,还包括原有程序依赖的能力怎样在目标环境中得到对应实现。分享介绍的是这些适配思路,并不意味着宿主提供了完整操作系统或任意本地文件访问权限。

具体到演示里的设想,一个 C 程序打开文件、写入内容再关闭,表面是几次标准库调用,背后却依赖文件系统。浏览器不能因为接收了一份 Wasm 模块,就允许它随意读写用户电脑。Emscripten 可以提供虚拟文件系统,让部分调用仍能工作;涉及线程、图形等能力时,还会根据浏览器可用的机制做相应衔接。于是构建产物可能不只有 .wasm,也有负责连接模块与浏览器能力的 JavaScript。于航想让听众看清的,是“编译成功”和“原程序的行为在新环境中仍成立”之间还有一段适配工作。

语言支持有不同的实现路径

讲到语言生态时,于航区分了两种常见路线。一种是把 C/C++、Rust、AssemblyScript 等语言的程序编译成 WebAssembly;另一种是先把解释器或运行时编译成 WebAssembly,再通过它执行另一种语言的程序。

这解释了为什么 Python、JavaScript 等语言也会出现在 Wasm 应用的讨论中。例如,嵌入一个编译为 Wasm 的 JavaScript 引擎,可以让某个系统接受脚本作为扩展逻辑。多一层运行时并不代表这种方案没有用途,关键在于应用需要什么执行方式与边界。因此,看到一门语言出现在支持列表里,还要进一步看它究竟以哪条路径运行。

于航特别提到 QuickJS 一类的引擎:若把解释器本身编译为 Wasm,宿主就可以在一个可控制的模块环境里运行脚本。它与“把同一段 JavaScript 源码直接编译成 Wasm”是两件事。前者多了一层,却可能满足插件、规则或可扩展逻辑的需要。他借这组例子提醒听众,不要把“支持某种语言”理解成所有语言都沿同一条编译路径、具有同样的运行特征。

从多媒体到云端:生态扩展也带来选型问题

随后,分享按领域介绍了当时的探索。面向 WebAssembly 设计的语言,尝试降低直接使用较底层语言的门槛;图像处理、音视频处理等场景,则把已有的计算逻辑和编解码能力带到网页中。仿真器的例子展示了另一种组合:用 Wasm 实现被模拟系统的核心逻辑,再配合浏览器的画布和事件处理,形成可以交互的应用。

他先展示了面向 Wasm 的语言尝试:与上一节直接看 WAT 指令相比,这些语言允许开发者用更熟悉的变量和函数写程序,再生成模块。它们要解决的是使用门槛,而不是宣称某种新语言能自动替代现有工具。谈到多媒体时,他举浏览器里的图片处理工具为例:原来要依赖本地软件完成的一些图片操作,如今可以放在网页中执行。音视频直播或播放中计算量较大的环节,也可能成为把已有代码编译到 Wasm 的候选。这里的具体收益需要针对处理任务测量,不能只凭“用了 Wasm”判断。

仿真器的例子更能看出分工。被模拟设备的指令或硬件行为由核心模块处理;用户看到的画面与输入事件仍要和浏览器接口结合。它不是把整个浏览器应用一键变成 Wasm,而是在一条交互链路中选择合适的部分交给模块。于航把这一点与游戏引擎中的物理计算放在一起:如果已有复杂计算或原生实现,可以考虑复用;界面、事件和平台能力仍需要另行接通。

云端部分涉及把 Wasm 工作负载接入容器调度环境;游戏领域关注引擎、物理计算等模块的复用和执行效率。区块链、物联网以及编译器、虚拟机等工具也被列入探索范围。它们共同说明,当时的讨论已经超出了单纯给网页加速的范围。

于航提到当时有人尝试让 Kubernetes 调度 Wasm 工作负载,也提到 Unity、Unreal 等游戏引擎对 Web 目标的支持,以及物理引擎中大量重复的数学运算。他把区块链、IoT、编译器和运行时也列在图上,但这些方向在分享中主要是概览,没有逐个给出生产效果或维护现状。它们可以告诉听众“有人在试什么”,不能等同于“这些领域都已经有成熟通用方案”。

但于航对实验性的前端框架保留了具体疑问。使用 Rust 等语言编写前端应用,需要考虑与 DOM 的交互方式,也需要考虑团队的学习成本和维护能力。如果掌握关键实现的人离开,其他成员能否接手?一种方案能够实现功能,并不足以证明它适合某个团队长期采用。这也是分享中从技术可能性转向工程选择的重要一段。

他的疑问来自实际团队运作:假设少数工程师熟悉 Rust 和相关框架,项目上线后由谁修改页面、排查问题、接手迭代?代码可以运行,只回答了“能不能做”;人才储备、调试工具和交接成本才会回答“值不值得做”。他没有否认这些框架作为实验的意义,但也不愿因为概念新,就跳过长期维护的检验。

eBay:把不同识别实现组合起来

eBay 的网页商品条码识别案例,把代码复用讲得更具体。分享中的架构图列出 ZBar、自有库和 JavaScript 库,由不同 Worker 执行。eBay 的原始工程文章说明,这些实现有各自擅长和不足的情况,因此最终方案组合多种识别方式,让能够取得有效结果的实现提供结果。

于航先还原了问题的来历:如果只用一个 JavaScript 实现,某些条码或图像条件下识别结果不理想;把已有识别库移到网页之后,也未必覆盖所有情况。最终方案不是寻找一段万能代码,而是让不同实现并行尝试。ZBar 和另一个原生实现借助 Wasm 进入浏览器,JavaScript 实现也留在系统里。各自运行在 Worker 中,能够识别的实现先给出可用结果。这也给不支持某项新能力的环境留下了其他路径。

WebAssembly 在这里帮助已有的原生库进入网页流程,JavaScript 实现则继续参与其中。工程价值同时来自代码复用与方案组合,并不只取决于单个函数是否运行得更快。eBay 的工程原文介绍的是 UPC 商品条码识别。

Shopify:性能、执行边界与语言选择

Shopify 的案例转向平台如何接受可编程业务逻辑。商家或合作方可以根据自己的需要提供优惠等规则,平台则要执行这些外部提交的程序。这套在 Shopify 2020 年文章中介绍的历史架构,以 AssemblyScript、WebAssembly 模块和 Lucet 执行服务连接这一过程。

于航先让听众想象一项具体业务:商家希望根据商品、时间或活动条件,编写自己的优惠逻辑。平台当然可以提供固定配置,但如果规则要通过用户提交的代码表达,就出现两个问题:平台需要一个可执行的格式,也不能直接把不受信任的程序放到自己的核心服务里运行。Wasm 模块在这套历史设计中承担中间格式的角色;平台再用专门的运行环境执行它,并控制模块可以调用哪些外部能力。

于航从几个方面解释这套设计。外部代码需要受到执行环境和宿主接口的约束;平台也要考虑处理业务请求的效率;以 Wasm 为中间形式,还可以给上层语言留下选择空间。分享同时提到了模块的导入关系,以及提前编译等执行准备方式。

他把安全、速度和语言选择分开讲。安全方面,用户提交的模块不能因为“只是一个模块”就自动获得宿主的文件、网络或业务数据;运行时要查看它依赖哪些导入接口,再决定提供什么。速度方面,业务请求可能频繁触发规则,加载、验证和编译的开销都值得考虑;提前编译是当时架构中采用的一种办法。语言方面,平台更关心最终提交的模块是否满足接口和限制,而不必要求所有开发者用同一种源语言。三者组合起来,才形成一个可供商家扩展业务逻辑的执行方案。

Shopify 的2020 年工程文章记录了这套历史方案。平台要组织好执行边界、依赖和业务接口;沙箱提供隔离机制,具体业务仍要管理好授权与数据使用。

四类提案,分别补足什么能力

讲完应用,于航回到标准与实现进展。他先介绍基础版本之后的提案及其推进过程,再选择四类能力展开。分享中的阶段与数量属于当年的快照,更值得保留的是它们分别试图解决什么问题。

引用类型与 externref。 示例让一个浏览器对象的引用进入 Wasm 模块,再通过模块导入的 JavaScript 函数完成操作。这个引用对 Wasm 来说是不透明的:能够传递它,并不等于模块已经理解 DOM 或能直接操作任意对象。示例通过宿主函数修改页面文字,展示的是宿主与模块怎样协作。

他在演示里把一个页面节点传给模块,再由模块调用导入的 JavaScript 函数,把节点文字改成示例内容。关键不是“Wasm 直接掌握 DOM”,而是它可以持有并传递一个宿主对象引用;真正修改页面的函数仍由浏览器侧提供。沿着调用顺序看,引用从浏览器进入模块,又随调用回到宿主函数,听众也就更容易理解为什么需要 externref。

线程与原子操作。 分享讨论多个实例使用共享线性内存的工作方式:不同 Worker 可以执行任务,但访问共享数据时需要处理并发协调问题。原子操作为这类协调提供基本能力,并不自动替程序消除所有数据竞争。

于航用几个 Worker 各自运行 Wasm 实例来解释并行处理。如果它们彼此没有共享状态,分配任务相对直接;当它们共同读写同一段内存,就可能在谁先读、谁先改上出现竞争。原子读写等指令提供了建立同步机制的基础,但任务怎样切分、什么时候等待、怎样防止错误覆盖数据,仍由程序设计负责。

固定宽度 SIMD。 相比逐个处理数据,向量指令可以让一条操作作用于多个数据通道。于航用成组数字运算解释这一点,并联系到图像像素、音视频等重复计算较多的场景。这里讨论的是数据并行能力,不能把向量的位宽误读成固定数量的数据通道。

他的例子是两组成对的数据需要分别相乘:如果每次只算一对,就得重复执行相同的操作;把数据装进固定宽度向量后,可以对多个通道使用同一类指令。128 位是向量总宽度,按 8 位、16 位或 32 位元素拆分,会得到不同数量的通道。图像处理中的一排像素、音视频中的重复数值运算,是听众容易联想到的场景;具体程序能否用上这种并行,还取决于数据组织和算法。

批量内存操作。 内存复制、填充等工作如果只能拆成许多细小操作,会给生成代码和执行带来负担。分享用 memory.copy 等能力说明,为什么标准需要直接表达整块数据的操作。这是对模块底层操作能力的补充。

于航把这一类提案放在很实际的编译结果里看:源程序里一次复制一块内存,如果目标指令集不能直接表达,工具链就要用较多指令拼出相同效果。提供批量复制、填充等操作后,模块可以更直接地说明意图。它不像一个用户可见的新界面功能,却影响底层库和编译器生成什么代码。

WASI:从接口声明走到具体实现

WASI(WebAssembly System Interface)部分,把关注点放在浏览器之外的系统能力上。于航先回顾传统程序的路径:标准库提供接口,接口最终需要通过特定操作系统的实现完成文件、内存等资源操作。表面相似的调用,在不同平台下会落到不同实现。

再看 Wasm,应用可以面向一组系统接口编写,运行时则负责把这些接口接到所在平台。分享把应用侧的接口声明,与 Wasmtime 中的具体实现放在一起对照,说明两者怎样对应。这个层次关系也解释了为什么模块本身可移植,并不意味着它能脱离宿主独立获得文件或其他系统资源。

于航把文件打开操作作为对照例子。传统 C 程序调用标准库函数时,库与操作系统最终要完成实际的资源操作;在 Wasm 的场景里,应用依赖一套约定好的接口,具体运行时再决定如何把请求接到当前平台。他展示了应用侧的函数签名,也展示了 Wasmtime 中用 Rust 编写的对应实现。两侧的名字和参数能对上,职责却不同:一侧告诉程序“可以怎样请求”,另一侧真正处理请求。这比只记住 WASI 的英文全称更能说明它为什么存在。

WASI 与核心计算指令的讨论由此形成分工:前者关注程序怎样接入外部系统能力,后者关注模块内部怎样表达和执行计算。分享还提到相关接口按不同领域推进,不能把当年的规划当成所有能力已经完成。

工具、社区调查与当时的使用建议

最后一部分先谈工具。即使某项能力已经进入提案或规范,也要检查目标浏览器、运行时是否支持;调试体验同样影响落地。于航介绍了当时在浏览器中调试 C/C++ 编写的 Wasm 应用的进展,并提醒开发者关注工具链与具体环境。他还提及 WebAssembly 2.0 草案、Docker 与 WasmEdge 的结合、Wasmtime 1.0,以及模块连接、接口类型和社区治理等生态动态。

分享展示的社区调查,从使用语言、未来意愿、应用场景和改进诉求几个角度观察趋势。于航的解读是:Rust 受到较多关注;JavaScript 的出现,需要结合把脚本引擎编译成 Wasm 的路线理解;Web 应用仍是重要场景,Serverless 和容器化等方向也在发展。开发者关心的既有线程、垃圾回收等能力,也有 API、调试与构建工具的完善。这里反映的是当时调查与讲者的解读,并非整个行业的市场份额结论。

他把几张调查图连起来看,而不是只读一个排名。已经使用过的语言与未来想使用的语言并不完全相同;看到 JavaScript 排在前面,也要想起前面讲过的“把解释器编译进模块”这一用法。应用场景上,Web 仍然占重要位置,云端、Serverless、容器化开始吸引更多关注。至于开发者希望优先改善什么,有人关注线程和垃圾回收这样的底层能力,也有人关心浏览器 API、调试和构建体验。对正在选型的团队来说,后者同样重要:一个能力在规范里写出来,不等于工程师已经能方便地开发和排错。

他的收尾建议是持续关注,并结合业务与团队条件选择使用。已有生产案例能证明某些问题有了可行解法,但具体项目仍要回答自己的需求、维护能力和运行环境问题。

成长对谈:从个人经历谈到怎样长期做技术

技术分享结束后,主持人把话题转向于航本人。下面按当时的提问顺序保留他的经历、例子和观点;对话发生在 2022 年,涉及工作与行业的判断也以当时为准。

从 VB、个人网站到前端工作

主持人的第一个问题是:为什么选择做前端?于航没有直接给出一个“从小立志”的答案,而是从初中讲起。那时舅舅给他买了一本介绍 VB 编程的书。他原先对计算机的认识更多是玩游戏;照着书尝试写程序之后,才发现自己也能用代码让机器做事。最早写的东西并不复杂,但这件事让他愿意继续找其他语言和工具来试。

后来他接触到 VBS,又逐渐转向 JavaScript。回忆这段经历时,他没有把它说成系统的学习路线:年纪小的时候,对脚本能做什么感到好奇,就会找各种例子来玩;等到开始做个人空间和网站,JavaScript 又成为更自然的选择。他给自己搭网站、做博客,也会往里面放文章。那些早期文章未必是成熟的技术写作,但做出一个能看、能改、能公开展示的网页,让他真正进入了 Web 开发。

聊到 VBS,他还想起少年时研究脚本的动机并不“高大上”:看到别人能用脚本做一些让电脑弹出提示或改变行为的小恶作剧,就想弄明白自己能否写出来。尝试的过程让他接触到更多资料,也慢慢发现这类脚本的用处有限。后来大家越来越多地使用 JavaScript,他也顺势转去研究网页。这个细节比一条整齐的“VB—VBS—JavaScript”学习路线更接近他的真实经历:好奇心常常先于明确的职业规划。

到了大学,他参加过一项需要用手机应用完成任务、和其他队伍比较完成速度的竞赛,因此做了一段 Android 开发。毕业时,这段经验也影响了他找工作的方向:他最初尝试过 Android 岗位。只是实际进入工作环境后,发现那份工作的氛围并不适合自己,于是重新寻找机会。这是他后来谈“职业选择”时反复强调的一点:真实经历往往会改变原先的打算。

他记得那份 Android 相关的实习只待了几天。不是突然不喜欢 Android,而是发现自己不愿长期处在当时那种工作氛围里。这个转弯把他带到了下一份用 PHP 做网站的工作,也说明为什么他不愿把每次转换包装成深思熟虑的战略决定:人在刚进入职场时,往往要先通过具体经历才知道什么环境适合自己。

下一份持续做下去的工作是用 PHP 开发网站。那时的项目不是清楚划开的前后端两套工作:他既写一部分 PHP 接口,也处理页面、样式和浏览器里的功能。做着做着,重心才逐渐落在前端。之后换团队、积累项目经验,前端成了主要工作方向;WebAssembly 则是他在工作之外长期钻研的兴趣,后来延伸到写文章、写书和分享。

主持人接着请他回看哪些节点最重要。于航提到最早的 VB 书、大学里的 Android 竞赛、毕业后的几次工作转换,以及后来持续投入 WebAssembly。它们不是一张预先画好的路线图:某项技术因为以前接触过,做起来有熟悉感;某个岗位因为工作环境不合适,就会促使人重新选择。主持人试着把这些节点概括为“遵循内心”,于航随即把这个说法拉回现实:并非每一步都能先想清楚、慎重规划后再执行,有些决定是在具体环境里做出的反应。他也不认为事后很容易判定当时哪一步绝对正确或错误。

当话题延伸到转行或刚入行的人怎样选方向时,于航补充了前端的一种吸引力:它能比较快地产生可见结果。搭出一个博客、改好一个页面,自己很快就能看到变化,也能把成果给别人看。这种“做出来了”的感觉,会推动初学者继续寻找更复杂的问题。他并没有因此说前端一定优于后端、系统编程或其他方向;他的意思是,对需要正反馈的人而言,能较快看到作品,是一个实实在在的学习动力。

写书的难处:写给谁,怎样保证准确

接下来的问题是《深入浅出 WebAssembly》为何会出现,以及写这本书对他意味着什么。于航说,自己起初并没有完整的出书计划,只是在研究 WebAssembly 时写过几篇博客。编辑联系他,询问能否把这些内容组织成书,他才认真考虑这件事。当时中文世界系统介绍这项技术的资料有限;如果能把自己摸索到的内容讲明白,也能让更多开发者接触它,理解它可能给 Web 开发带来什么变化。

答应写书之后,困难才真正显现。首先是读者定位:前端开发者可能缺少底层知识的背景,直接从指令、编译和运行时讲起会很吃力;已经熟悉这些内容的人,又可能觉得铺垫太多。一本书不能像单篇博客那样只围绕一个问题展开,还要决定前后章节的顺序、哪些概念先讲、解释到什么深度。于航说,那时可供对照的同类中文书不多,很多取舍只能基于自己对技术和读者的理解。

这本书最后写了数百页,前后花了大约七八个月。于航回忆,最难的并不是坐下来写了多久,而是写到每一章都要重新问:读者已经知道什么?这里若直接引入底层概念,会不会让前端读者失去线索?若先讲太多基础,熟悉编译和运行时的读者又会不会觉得没有新内容?当时缺少可直接照搬的中文书结构,他只好一边查资料,一边试出适合这本书的讲述顺序。

他会从官方资料和社区讨论中筛选值得介绍的内容,再想清楚怎样把标准、工具和应用连成读者能够跟上的路径。研究中写下的博客提供了起点,但把材料放进一本书,还需要不断重新组织和解释,并不是把旧文章接在一起就能完成。

第二个困难是确认自己写得对不对。于航回忆,当时一些文档更像写给标准制定者或实现者看的材料;若没有相关设计背景,普通开发者很难只读一遍就准确理解。他会在 Stack Overflow、GitHub 的讨论区提问;认识相关工具的开发者后,也会把一段时间积累的问题集中起来,通过邮件请教。拿到别人给出的解释,他仍会回头看规范、做实验,检查自己的理解能否在代码里成立,再决定怎样写进书里。这是比“找到一个答案”更耗时的一层工作。

主持人由此给想写书的开发者总结了一条路径:先把博客写好,逐渐形成稳定的内容,再考虑更大的写作项目。于航认可写博客作为起点,但他的故事也说明,能写出一本书还取决于知识组织、资料核实和持续投入。书名与作者信息可见于他此前撰写的课程开篇词。

写作和分享,都从能讲清一个小问题开始

主持人接着问,有技术积累的人怎样把内容写出来、分享出来。于航给出的办法并不玄妙:先观察别人怎样写,再自己反复练。读书、读博客时,他会留意作者怎样安排章节、怎样从一个概念过渡到另一个概念。轮到自己研究一项新技术,也试着把它写成文章,回头检查读者能否沿着这个顺序理解下去。

练习不一定从长文开始。他建议挑一个刚学会的小知识点,先讲它是什么,再补足理解它所需的背景,最后说明它怎样进入项目,或者还能引出哪些问题。一个小知识点也可以写出清楚的开头、展开和结尾。写完后再读,检查有没有突然跳到读者尚不知道的概念、有没有把解释顺序颠倒。

他拿自己的写书经历作比较:早期章节与后面几章在组织和语句流畅度上并不一样。写得越多,回头读得越多,越能意识到哪里讲不通。练习带来的不仅是文字更顺,也包括从整体和细节两个角度检查内容的能力。

他给出的练习方法很小:比如今天刚弄懂一个知识点,不必等到理解整个技术体系才动笔。先写清这个知识点本身,再写它与哪些问题相关,最后写它在自己的项目里能解决什么、还留下什么疑问。读者能否顺着这条线理解,是比文章长度更直接的检验。写过几篇后,再回看自己从第一篇到后面几篇怎样调整结构,进步也会变得可见。

讲到公开分享,他把练习的起点放在人数较少、题目较具体的沙龙。一个小主题、一个自己刚解决的问题,就可以讲给熟悉的听众听;人少时更容易调整紧张情绪,也能发现准备好的内容哪里不清楚。开始写一份较完整的逐字稿,他认为完全可以接受。熟悉主题以后,再试着只在幻灯片上列出几个关键点,围绕这些点现场组织表达。

于航特别提到,始终盯着逐字稿念,有时反而会让思路跟不上口头表达。逐渐从稿子转向提纲,是为了在讲的时候继续思考,而不是背诵一份写好的文章。这个过程没有速成技巧:从紧张、不会组织内容,到能面向更多人讲清楚,靠的是不断练习。

正式演讲之外,他还提出一个更容易开始的练习场景:日常会议。能否把一个想法向团队成员讲清楚、让别人理解自己的判断和方案,同样是在训练分享。不是非要站上舞台、准备一个完整的主题,才算练习表达;工作中反复进行的小型说明,会慢慢形成更稳的表达能力。

兴趣与技术能力,不必被刻板印象捆在一起

对话中段,主持人把话题转向程序员工作之外的兴趣,提到技术圈中一些人的特别爱好,并问这种爱好和技术能力是否有关。于航并不认同从几个熟人的例子推导出因果关系:恰好认识的人技术不错,又恰好有某种爱好,不足以说明爱好使他们成为更好的工程师。他把它和玩游戏、追剧、看动漫放在一类:对当事人而言,这就是工作之外让自己放松的事情,只是有些兴趣在旁人眼里显得更特别。

主持人提问时用了技术社区里“女装大佬”的说法。他观察到,认识的一些有这类兴趣的开发者技术也不错,于是半开玩笑地问两者是不是有关。于航没有顺着这个玩笑推出一种“程序员规律”:人们会同时拥有技术能力和各种爱好,不能因为几个熟人恰好符合,就认为穿着或兴趣解释了技术水平。把这一段放回访谈里看,它不是职业建议,而是两个人借一个轻松话题谈程序员是否只能有单一形象。

主持人解释,提问还有一层意思:外界常把程序员想象成沉默、无趣、只会盯着电脑的人,而他认识的同行有人擅长音乐、绘画或公开表达,也有各式各样的生活爱好。于航的回应没有试图再给程序员贴一个相反的标签。他只是提醒,人们在工作中高度专注,不代表工作之外也是同一个样子;爱好与技术角色可以同时存在,不需要把前者解释为后者的原因。

团队文化:共同目标、多元背景与业务取舍

进入工作话题后,主持人请于航比较不同环境中的工程师文化。于航首先谈到工作与生活的平衡,但没有停在一句“外企更轻松”。他描述的是自己当时在 PayPal 团队中的感受:同事和负责人会围绕共同的业务目标讨论工作,大家也愿意在具体事情上投入时间。他不希望把团队协作理解成每个人只守着自己的绩效指标,再因利益冲突而互相消耗。

他还提到团队定期接受内部培训,了解哪些行为不被允许,以及遇到问题时有哪些反馈渠道。这些规范并不会自动保证所有人的经历完全相同;于航讲的是自己周围的工作氛围,不能当作 PayPal 在所有地区、所有时期的统一现状。

国际化协作带来的另一个变化,是同事的背景不再相近。于航举自己团队的例子:开会前闲聊,就会听到不同地区同事的生活和工作经历;进入项目讨论,不同人处理同一件事的习惯也可能不同。过去熟悉的一套沟通方式未必能直接搬过去,既要理解公司的规则,也要留意对方的文化与表达习惯,避免以自己的经验预设别人会怎样想。

这种多元让他觉得有趣,也要求更多适应。主持人随后提醒,听众未必都喜欢同样的工作方式;于航认可这一点。多元、弹性、清晰的业务边界,对一些人是优势,对另一些人未必如此。访谈没有把一种环境说成所有工程师的最佳选择。

谈到“工程师文化”具体体现在哪里,于航把话题拉到资源分配。PayPal 的主业是支付及相关业务;他所在的团队做与自身职责相关的应用和服务,帮助业务流程运转。另一类需求,例如文档协作、办公流程或团队沟通,如果市场上已有合适产品,就可能购买服务,而不是专门组建团队重做一套。他用常见的文档和沟通工具举例,说明这是一种把工程力量集中到主营业务的选择。

主持人借此比较一些公司倾向于自建内部工具的做法。于航并没有说自建一定错误,而是强调组织需要回答:这项能力是不是我们的核心竞争力?自己做能带来什么收益?购买现成服务、找外部团队解决是否更合适?这类问题会影响工程师最终把时间花在哪里。

他随后补充,这不等于公司没有基础设施建设或技术投入。前端、服务端、内部工具及相关标准社区的工作仍在进行;只是这些工程活动最终要服务于实际业务,而不是为了显得“大而全”就什么都自己做。谈到工作环境时,他也提及了当时的股票、保险等员工配套,但访谈没有给出适用于今天或所有岗位的具体条件。

选择工作环境之前,先了解真实的日常

于航还讲了自己刚进入团队时的适应过程。起初,他会对一些工作安排格外谨慎,例如担心错过会议是否需要逐级解释。后来他发现,同事会先假定对方有合理原因,工作更依赖个人主动安排和交付。这个例子重点不在某一条请假或会议制度,而在于他对信任与自驱的感受发生了变化:适应新团队,除了学技术,也要学会理解别人怎样看待沟通、责任和时间安排。

他记得,有一次担心错过会议,甚至想着是否要专门给负责人写一封解释邮件。对方的反馈却比他预想得平和:没有参加,大家会先认为他可能被别的紧急事务占住;真正重要的是工作有没有被妥善处理。这件小事让他意识到,自己沿用上一种环境里的谨慎方式,未必适合眼前团队。他说适应这种节奏大约用了半年,除了熟悉系统和业务,也在重新学习怎样主动安排工作。

主持人补充,另一类节奏更紧的团队,可能让工程师较早接触核心技术、在短时间内积累另一种经验,所以“喜欢哪种公司”不能只用工作氛围回答。于航的建议是先了解人在那里实际怎样工作和生活,再决定自己要不要进入那个环境。听说某家公司好、看到朋友去了国外,或者只看到某种福利,都不足以替代对日常任务、团队协作、生活变化和文化适应的判断。

这也是他不愿给出简单职业模板的原因:同一家公司里,不同团队可能有不同目标;同一个工作方式,也可能让两个人获得截然不同的体验。选择之前,多看真实经历,再回到自己的需要上来。

岗位准备:技术基础之外,还要能理解和回应

主持人接着问,如果听众想寻找类似岗位,应准备什么?于航先把讨论拉回具体职位:团队会看岗位需要什么,也会看候选人在简历里写了什么经历,再围绕这些内容提问。按他当时参与面试的经验,问题不会只是一套与工作无关的记忆题;技术能力当然要扎实,包括基本的算法和软件开发知识,但面试也应帮助双方判断实际能不能一起完成工作。

他特别提到,沟通能力在问答过程中自然会暴露出来。面试官提出一个问题,候选人是否听懂了重点?回答是否沿着问题推进?如果对方补充条件或给出反馈,能否调整自己的解释?于航认为,这些表现与入职后的协作直接相关:面对同事或负责人提出的问题,既要理解对方真正需要什么,也要能说明自己准备怎样处理。只会背答案,不能代替这种双向沟通。

英语要求则要看职位和协作范围。于航说,如果团队只在本地协作,所需能力与面向全球产品、经常与不同地区同事沟通的岗位不会一样。对后者而言,读写是基础;哪怕不天天开英文会议,遇到紧急问题时也要大致听懂对方在说什么,回应现状、方案和下一步。岗位越需要带领团队或对更多人讲话,要求越不只是“能翻译”——还包括怎样向不同受众组织信息,怎样把困难消息讲清楚而不制造误解。

他把不同层级的沟通场景讲得很具体:工程师可能需要读文档、写说明,偶尔与海外同事处理一个临时问题;负责更大团队的人,则可能要在更多人面前解释决定、提供反馈,甚至传达坏消息。后者遇到的难题不只是听懂词句,还包括语气、受众和时机。主持人随即说,自己从中文工作环境转向国际化团队时,即便知道要表达的意思,换成英文也会感觉信息打了折扣。两人的经验都指向同一件事:真正的工作沟通能力,需要长时间在具体场景里练。

于航给有明确目标的人一个朴素建议:如果已经打算应聘需要英语的职位,可以提前半年开始,每天留半小时或一小时练词汇、听力和表达。没有持续使用英语的环境,进步可能更慢,就更需要自己安排练习。他把这和公开分享放在一起:从不敢开口到能把复杂事情讲清楚,都不可能靠临时突击完成。

主持人用自己的经历接上这个话题:同样一件事,用中文可能表达得很自然,换成英文后,即使每个词都知道,也会丢失语气和细节。他认为这类能力需要在真实沟通中磨练。于航同意,并提醒有相关职业打算的人尽早准备:词汇、听力和口语都需要持续积累,无法指望临近面试时几天突击解决。

这段建议来自他 2022 年的工作经验,不是今天 PayPal 的招聘标准。它更普遍的含义是:先看清岗位日常真正用到的沟通场景,再为那些场景做长期准备。

学习可迁移的知识,也按需要深入源码

访谈最后,主持人问前端开发者接下来怎样成长。于航没有给一份“未来几年必学技术”名单。他把问题扩展到所有软件开发者:学习要持续,但更要分清哪些知识容易随版本变化,哪些知识能在多个项目、语言和框架之间迁移。算法、架构设计、程序结构、编码方式等,在他看来都属于值得长期打底的部分。

他用两种学习框架的方式作对比。一个人不断追着某个版本的源码,记住函数在哪个文件、用了哪种语法;另一个人也看实现,但同时追问框架为什么这样划分职责、为什么选这个设计。等到一年后框架换了实现,或者工作转向另一个框架,前者记住的位置和写法可能很快过时,后者理解的设计原则仍能帮助他分析新代码。真正能迁移的不是某一版源码本身,而是对问题、约束和结构的理解。

他让听众想象从框架 A 转去框架 B 的场景:两者源文件、语法和 API 可能完全不同,如果只记住 A 的某个函数写在哪里,很难给 B 的设计提出建议;如果理解了 A 为什么这样划分模块、解决了什么约束,再看 B 时就有了可以比较的问题。于航并不是要求开发者放弃具体实现,而是提醒他们在读源码时顺手问一句“这一层为什么存在”。这样积累的知识,才有机会在工具变化后留下来。

于航随即补充:这并不是说源码不该读。遇到工作中的具体问题、自己的项目需要弄清某个实现,或者对某项技术确实感兴趣,完全可以深入到源码层。他反对的是把不断追逐易变实现当成唯一学习方法。先建立较稳定的知识基础,再根据问题走进实现细节,投入通常更有回报。

架构能力从代码开始,成长也需要机会

说到架构能力,于航提醒听众不要被“架构师”这个称号带偏。很多人一听架构,就想到一个人设计庞大的分布式系统、管理成百上千的服务,仿佛只有到了那个层级才开始做架构。他认为,写一个普通功能时,怎样抽象、怎样分层、组件之间如何协作,本来就是架构问题。一个人如果连几百行代码中的职责都分不清,让改动牵一发动全身,仅有“架构师”的头衔也不能替他完成更大的系统设计。

他给出的练习起点很具体:面对一段“面条代码”,试着找出不同责任,把相互缠绕的逻辑分开;检查组件之间的依赖能否说清楚,后续增加功能时是否容易扩展。代码层面的结构做得稳,再把同样的思考扩展到服务之间、系统之间。架构能力不是离开编码之后突然获得的,而是在日常写代码时一点点练出来。

主持人的问题也涉及未来几年该押注什么方向。于航没有给出确定答案。他拿 WebAssembly 发展过程作例子:人们曾预期一些能力会很快成熟,实际推进却涉及提案、实现和生态配合,很难在今天准确说出某项技术两三年后会到哪一步。与其押注某个热门词一定成为下一片蓝海,不如把能迁移的基础、解决问题的硬技能,以及沟通协作的软技能持续积累起来。

但他也不把职业成长讲成“能力够了就一定升职”。组织结构里可能暂时没有相应岗位;即使一个人已经具备能力,也不会因为满足某张清单就自动得到新的职责。层级越高,所需影响力和机会越难用简单指标量化。个人能做的,是保持学习和竞争力,了解所在组织如何运作;当团队出现需要有人承担更大责任的机会时,自己已准备好去争取。如果机会暂时不存在,不能把这件事简单归结为个人还不够努力。

完整录播

查看本期原视频与分段信息:于航:WebAssembly 2022 回顾与前端专家成长 →