Lit 模板编译优化全解析:@lit-labs/compiler TypeScript Transformer 的演进、原理与实战
【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/lit
@lit-labs/compiler是 Lit 官方实验室(Lit Labs)推出的构建期模板编译器,其核心产物是一个 TypeScript Transformer,可在编译阶段将html标签模板静态"预编译",从而消除 lit-html 运行时中耗时的prepare 渲染阶段,让模板密集型应用的首次渲染更快。本文以 packages/labs/compiler/CHANGELOG.md 的版本演进为主线,结合仓库源码与测试用例,系统讲解该编译器的设计动机、接入方式、可编译边界与权衡,帮助你在自己的构建管线中正确评估并启用这一优化。
一、从 CHANGELOG 看版本演进:一个逐步成熟的构建期优化器
@lit-labs/compiler的版本历史清晰地记录了它从实验性发布走向稳定的全过程。CHANGELOG 中披露的关键节点如下:
| 版本 | 类型 | 核心变更 |
|---|---|---|
| 1.0.0-pre.0 | Major | 初次发布,vending 一个 Lit 模板 TypeScript transformer;TypeScript 更新至 ~5.2.0(PR #4141) |
| 1.0.0 | Major | 正式发布(PR #4128),对外提供 Lit 模板 TypeScript transformer |
| 1.0.1 | Patch | 修复编译过程中相邻 attribute part 与 element part 值相互混淆的 bug(PR #4348) |
| 1.0.2 | Patch | 更新 TypeScript 依赖;lit-html 升级至 3.1.2 |
| 1.0.3 | Patch | 依赖的 @lit-labs/analyzer 升级至 0.12.0 |
| 1.1.0 | Minor | TypeScript 升级至 5.5.0;lit-html 升级至 3.2.0,analyzer 升级至 0.13.0(PR #4682) |
| 1.1.1 | Patch | 在 README 中补充 Lit Labs 声明(PR #4903) |
| 1.1.2 | Patch | TypeScript 依赖更新至 5.8,并处理相关的 ARIAMixin 类型变更(ariaColIndexText、ariaRelevant、ariaRowIndexText);analyzer 升级至 0.14.0(PR #4984) |
从依赖关系可以看到,该编译器并非独立工作:它深度依赖 @lit-labs/analyzer(类型分析与模板解析)、lit-html(模板运行时的私有支持模块)以及 TypeScript 的类型系统。当前仓库中 package.json 声明的运行时依赖为@lit-labs/analyzer@^0.14.0、@parse5/tools@^0.3.0、lit-html@^3.2.0、parse5@^7.1.2、typescript@~5.9.0,其中 parse5 系列用于在 Node 环境中解析 HTML——这是编译期替代浏览器行为的关键一环。
值得注意的还有 1.0.1 修复的那个 bug:相邻 attribute part 与 element part 的值在编译期间可能被混淆。这类问题说明编译器需要在语法层面精确还原运行时 parts 的顺序与归属,这也是它被归类为 Labs(实验性)包的原因之一。
二、核心原理:在构建期消除 lit-html 的 prepare 渲染阶段
要理解这个编译器,首先要理解 lit-html 的渲染模型。lit-html 模板渲染分为两个阶段:prepare(模板准备)与render(渲染提交)。prepare阶段会解析模板字符串、建立 DOM 骨架、并为每个插值位置创建对应的 part 数据结构;这个阶段在每次模板首次使用时会重复执行。对于模板数量多、复用频繁的应用,这部分开销会累积成可感知的首次渲染延迟。
@lit-labs/compiler的思路非常直接:把 prepare 阶段从运行时搬到编译期。它导出的compileLitTemplates是一个 TypeScript Transformer,入口定义在 packages/labs/compiler/src/index.ts,真正实现位于 src/lib/template-transform.ts。该文件中的CompiledTemplatePass类完成了整个变换流程:
- findTemplates:遍历 AST,借助类型检查器(
getTypeChecker,见 src/lib/type-checker.ts)识别所有确属 Lit 模板的html标签模板表达式; - litHtmlPrepareRenderPhase:将模板静态字符串"伪造"成
TemplateStringsArray,调用 lit-html 私有支持模块(lit-html/private-ssr-support.js)中的getTemplateHtml生成带 marker 的 HTML,再用 parse5 解析成 AST,逐个节点遍历生成 parts 列表,最后重新序列化为精简的 prepared HTML; - rewriteTemplates:把每个
html\...`表达式替换为对编译产物的引用(CompiledTemplateResult),并在模块顶层注入CompiledTemplate` 定义。
源码注释明确指出,litHtmlPrepareRenderPhase的逻辑与 lit-html 内部Template类的 prepare 逻辑几乎一致,只是用 parse5 取代了浏览器 DOM API。为了控制产物体积,编译时 marker 会被剥离,注释节点统一压缩为 3 字节的<?>格式。
以仓库中的 basic.ts 为例,输入源码:
import {html} from 'lit-html'; export const sayHello = (name: string) => html`<h1>Hello ${name}${'!'}</h1>`;经过编译器处理后(见 golden 文件 basic.golden.js):
import { html } from 'lit-html'; const b_1 = i => i; const lit_template_1 = { h: b_1 `<h1>Hello <?><?></h1>`, parts: [{ type: 2, index: 1 }, { type: 2, index: 2 }] }; export const sayHello = (name) => ({ ["_$litType$"]: lit_template_1, values: [name, '!'] });可以看到:模板字符串被替换为预编译的lit_template_1(其中h是安全标记 tag function 包装后的 prepared HTML,parts描述了插值位置与类型),html\...`表达式被替换为{_$litType$: lit_template_1, values: [...]}结构。运行时不再需要解析模板字符串、定位 DOM 节点,直接按parts` 索引更新即可,这就是首次渲染提速的来源。
三、接入方式:在 Rollup 构建管线中启用编译器
由于本质是 TypeScript Transformer,它可以被用在任何接受 transformers 的构建工具中。仓库官方推荐的是 Rollup +@rollup/plugin-typescript组合(见 packages/labs/compiler/README.md):
// rollup.config.js import typescript from '@rollup/plugin-typescript'; import {compileLitTemplates} from '@lit-labs/compiler'; export default { // ... plugins: [ typescript({ transformers: { before: [compileLitTemplates()], }, }), // other rollup plugins ], };仓库内还有一个可直接参考的完整真实配置:rollup.source_map_tests.js。它演示了如何在 Rollup 中配置 transformer,同时开启sourcemap: true与preserveModules: true,用于验证编译后的产物仍能保持合理的 source map 映射——这对生产环境排错非常关键。该配置的测试输入位于 test_files/source_map_tests/basic.ts,对应的 tsconfig 为 test_files/source_map_tests/tsconfig.json。
此外,编译器的构建与测试均通过 wireit 组织(见 package.json):
# 构建编译器(含 source map 测试所需的 rollup 构建) npm run build -w @lit-labs/compiler # 运行测试(uvu 测试框架) npm run test -w @lit-labs/compiler # 更新 golden 文件(通过环境变量 UPDATE_LIT_COMPILER_GOLDENS=true 触发) npm run update-goldens -w @lit-labs/compiler其中update-goldens脚本的files字段排除了*.golden.js,意味着这些 golden 文件是测试的预期输出、由工具自动维护,改动实现后需要显式更新它们。
四、如何验证优化已生效
优化是否生效,最直接的判断标准是:构建产物中是否还存在html标签函数调用。官方 FAQ 给出了判断方法——如果源码中有:
const hi = (name) => html`<h1>Hello ${name}!</h1>`;那么构建后这段代码应被转换为类似下面的形式(html调用被消除):
const b = (s) => s; const lit_template_1 = {h: b`<h1>Hello <?></h1>`, parts: [{type: 2, index: 1}]}; const hi = (name) => ({_$litType$: lit_template_1, values: [name]});仓库中的 golden 测试文件(*.golden.js)正是这一验证过程的自动化形式:每个测试用例输入文件(如 basic.ts)都对应一份预期输出(如 basic.golden.js),测试通过比对编译结果与 golden 文件来判断优化逻辑是否符合预期。如果你想在自己的项目中确认效果,可以在构建前后分别搜索产物中html标签函数与lit_template标识符的出现情况。
五、哪些模板会被编译:可编译性边界
并非所有html模板都会被编译。编译器有一套严格的可编译判定规则,这些规则在 template-transform.ts 与 README 中有明确阐述,并且在 test_files 目录下几乎每个规则都有对应测试用例:
1. 必须是结构良好的模板
模板不能包含会在 lit-html 开发构建中触发运行时诊断的表达式(例如位于非法位置的表达式)。从源码看,以下几类情况会直接放弃编译(shouldCompile = false):
- 模板静态字符串中包含已废弃的八进制转义序列(如
\1、\8、\9)。源码中的templateContainsOctalEscapes与containsOctalEscapeRegex专门处理此场景——因为未编译的模板由html标签函数在运行时检测此类错误,而编译后 tag 函数被移除,错误会被静默渲染成undefined文本(相关测试见 handle_deprecated_octal_escape.js); - marker 被插入到标签名位置(即动态元素名,见 dont_compile_invalid_dynamic_element_name.js);
template与textarea元素的 innerHTML 中包含动态绑定(源码中的elementDoesNotSupportInnerHtmlExpressions集合)。
2.html必须直接导入自lit或lit-html
编译器通过类型检查器确认 tag 函数的来源,因此从其他模块 re-export 的html不会被视为 Lit 模板。支持以下三种导入形式:
import {html} from 'lit'; // 直接导入 import {html as litHtml} from 'lit'; // 别名导入,litHtml`...` import * as litModule from 'lit'; // 命名空间导入,litModule.html`...`对应的测试用例包括 basic.ts、handle_aliased_imports.js、import_from_namespace_compiles.js,以及从lit与lit-element不同入口导入的用例(basic_lit_import.ts、basic_litelement.ts)。
3. 原始文本元素内不能有动态绑定
textarea、title、style、script这四个原始文本元素(raw text elements)的内容在 HTML 解析中按原始文本处理,其子节点无法作为相邻子节点被正确放置,因此包含动态绑定的此类模板不会被编译。源码中的rawTextElement正则^(?:script|style|textarea|title)$与 README 描述一致,测试见 raw_text_element.js 与 raw_text_element_edgecase.js。
4. 其他被正确处理的边界场景
- SVG 模板不编译(dont_compile_svg.js);
- 被遮蔽的
html标识符不会被误编译(dont_miscompile_shadowed_html.js)、html标识符被重新赋值时同样被跳过(handle_html_identifier_reassignment.js); - 嵌套模板会被内联提升(inlined_nested_templates.ts、pass_inlined_template_into_template.ts);
- 嵌套作用域中的模板会被提取到模块顶层(compiled_templates_pulled_out_of_nested_scopes.golden.js);
- 注释节点中的绑定会被记录为不活跃 part(
COMMENT_PART),以保证后续绑定索引正确(comment_binding.js)。
六、五种 part 类型与 kitchen sink 全景用例
编译器生成的parts数组是运行时更新 DOM 的依据,PartType枚举定义了每种插值的类型。以 parts_kitchen_sink.js 这个"全家桶"输入为例:
import {html} from 'lit'; import {ref} from 'lit/directives/ref.js'; html` <div attribute-part=${'attributeValue'} ?boolean-attribute-part=${true} .propertyPart=${'propertyValue'} @click=${() => console.log('EventPart')} ${ref(console.log)} > ${'childPart'} </div>`;其编译产物(parts_kitchen_sink.golden.js)展示了完整的 part 构造逻辑:
import { html } from 'lit'; import { ref } from 'lit/directives/ref.js'; import * as litHtmlPrivate_1 from "lit-html/private-ssr-support.js"; const { AttributePart: A_1, PropertyPart: P_1, BooleanAttributePart: B_1, EventPart: E_1 } = litHtmlPrivate_1._$LH; const b_1 = i => i; const lit_template_1 = { h: b_1 ` <div> <?> </div>`, parts: [{ type: 1, index: 0, name: "attribute-part", strings: ["", ""], ctor: A_1 }, ... ] };编译产物从lit-html/private-ssr-support.js导入_$LH中对应的 part 构造器并赋予唯一短名(A_1/B_1/P_1/E_1),将各 part 的name、静态strings片段与ctor一起写入parts数组。结合 template-transform.ts 中PartType的定义与createTemplateParts的实现,可以归纳出完整的 part 类型映射:
| 源码语法 | PartType | 说明 |
|---|---|---|
attr=${v} | ATTRIBUTE(ctor 为 AttributePart) | 普通属性绑定 |
.prop=${v} | ATTRIBUTE 变体(ctor 为 PropertyPart) | 属性(property)绑定 |
?bool=${v} | ATTRIBUTE 变体(ctor 为 BooleanAttributePart) | 布尔属性绑定 |
@event=${v} | ATTRIBUTE 变体(ctor 为 EventPart) | 事件监听绑定 |
${dir(...)}位于元素上 | ELEMENT | 元素 part(如指令ref) |
文本/注释中的${v} | CHILD / COMMENT_PART | 子节点 part |
源码中的解析逻辑通过正则/([.?@])?(.*)/匹配属性名前缀来决定 ctorType:.前缀映射 PropertyPart,?前缀映射 BooleanAttributePart,@前缀映射 EventPart,否则为 AttributePart。这个映射保证了编译产物与 lit-html 运行时的 part 语义完全一致。
七、权衡与 FAQ:启用编译器前必须知道的事
README 的 FAQ 部分明确了启用编译器的代价与边界,这些属于官方文档直接声明的项目事实:
权衡(Tradeoffs):
- 使用编译器要求你的构建管线能够接受 TypeScript transformer——纯无构建流程(如直接引入 ESM 模块)无法使用;
- 收益:首个模板的首次渲染更快(官方表述为模板密集型页面最多可快约 45%);代价:当前产物文件体积大约增大 5%(gzip 后)。体积增大的原因之一是编译产物需要内联 prepared HTML 与 parts 元数据,因此这是一个"以体积换首帧速度"的取舍。
JavaScript 文件支持吗?
支持。由于 JavaScript 是 TypeScript 的子集,该 transformer 的实现在设计时就考虑并测试了对纯 JS 文件的处理,你只需要把 transformer 也应用到.js文件上即可。
版本依赖与兼容前提:
根据 package.json 与 CHANGELOG,当前版本(1.1.2)依赖lit-html@^3.2.0、@lit-labs/analyzer@^0.14.0与typescript@~5.9.0,Node 引擎要求>=14.17。由于它深度耦合 lit-html 的内部实现(private-ssr-support.js)与 TypeScript 版本,升级这些依赖时需同步关注编译器版本(CHANGELOG 中 1.1.2 对 TypeScript 5.8 的 ARIAMixin 类型适配就是一个实例)。
八、未来工作:官方规划中的优化方向
CHANGELOG 与 README 还披露了编译器团队明确的后续方向,可帮助你判断该工具的发展路线与适用时机:
- 减小输出体积:研究如何降低编译产物带来的文件体积增幅;
- 扩展 tree-shaking:实现对 Lit 应用的整体编译,从而把不再需要的 lit-html 运行时也一并摇树剔除;
- 探索 prepare 阶段之外的更多优化:目前优化仅覆盖 prepare 阶段,后续可能扩展到其他渲染环节;
- 提供更多使用与消费方式:目前以 TypeScript Transformer 形式分发,未来可能提供不同形态的接入方式。
需要特别提醒的是,@lit-labs/compiler属于 Lit Labs 系列包(与 packages/labs/analyzer、packages/labs/compiler 同属实验阵营):它发布的目的在于收集设计反馈,可能出现破坏性变更或停止维护(这一点在 1.1.1 中甚至被写入了 README 醒目位置)。生产环境采用前,务必阅读 packages/labs/compiler/README.md 中的免责声明,并结合自身的模板密度、首屏性能预算与包体积预算做针对性评估。如果你的应用以模板渲染性能为主要瓶颈、且构建管线成熟可控,这个编译器是目前 Lit 生态中官方提供的、有源码与测试双重背书的最直接优化手段。
【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/lit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考