这是【零基础 Rust 入门】系列的第 7 章,也是最后一章。本系列由前端技术专家零弌分享。想要探索前端技术的无限可能,就请关注我们吧!🤗
- 🧑💻 我们是谁:支付宝体验技术部-基础服务团队
- 📖 我们的专栏:前沿视点
lol-html
Low Output Latency streaming HTML rewriter/parser with CSS-selector based API.
基于 CSS 选择器、低输出延迟、流式 HTML rewriter/parser。
两大特性:
-
友好的 API:基于 CSS Selector 对前端友好
-
性能:性能极快,最会在必要的时候才会和 JS 进行交互
环境准备
git clone git@code.alipay.com:zhubin.gzb/lol-html.git
运行 demo
// 切换到 defer 分支
git checkout examples/defer
// 运行 demo
cat examples/defer_scripts/index.html | cargo run --example=defer_scripts > examples/defer_scripts/defer.html
查看 demo 效果,html 中 script 增加了 defer attribute。

Low Output Latency streaming HTML rewriter/parser with CSS-selector based API.
基于 CSS 选择器、低输出延迟、流式 HTML rewriter/parser。
两大特性:
- 友好的 API:基于 CSS Selector 对前端友好
- 性能:性能极快,最会在必要的时候才会和 JS 进行交互
# 环境准备
```shell
git clone git@code.alipay.com:zhubin.gzb/lol-html.git
运行 demo
// 切换到 defer 分支
git checkout examples/defer
// 运行 demo
cat examples/defer_scripts/index.html | cargo run --example=defer_scripts > examples/defer_scripts/defer.html
查看 demo 效果,html 中 script 增加了 defer attribute。
< <script src="http://.../simple-example.js"></script>
---
> <script src="http://.../simple-example.js" defer=""></script>
demo 代码
使用 css selector script[src]:not([async]):not([defer]) 选择 element。设置 attribute defer。
// Create the rewriter
let mut rewriter = HtmlRewriter::new(
Settings {
element_content_handlers: vec![element!(
"script[src]:not([async]):not([defer])",
|el| {
el.set_attribute("defer", "").unwrap();
Ok(())
}
)],
..Settings::default()
},
output_sink,
);
性能优势
(LazyHTML 是 LOL HTML 的上一代实现)
lol-html 提供了两个 Parser,Tag Scanner 和 Lexer。
- Tag Scanner: 只做 Tag 匹配,不匹配直接跳到尾部
- Lexer: 全匹配,包括 tag、attribute
实现细节
CSS Selector
parser & selectors
感谢一波赛博佛祖 Mozilla。
servo.org/about/ Servo is a web rendering engine written in Rust, with WebGL and WebGPU support, and adaptable to desktop, mobile, and embedded applications.
Servo 是一个基于 Rust 开发的 web 渲染引擎并有 WebGL、WebGPU 支持,可以适配桌面、移动端、嵌入式应用。
Servo aims to provide an independent, modular, embeddable web rendering engine, allowing developers to deliver content and applications using web standards.
Servo 的目标是独立、模块化、可嵌入,使开发者能够基于 web 标准分发内容和应用。
Created by Mozilla Research in 2012, the Servo project is a research and development effort. Stewardship of Servo moved from Mozilla Research to the Linux Foundation in 2020, where its mission remains unchanged. In 2023 the project moved to Linux Foundation Europe.
Servo 与 2012 年由 Mozilla Research 创建,是一个研究性和开发性项目,2020 交由 Linux Foundation 管理,2023 年交由 Linux Foundation Europe 管理。其目标一直未发生改变。
Servo is written in Rust, taking advantage of the memory safety properties and concurrency features of the language.
Servo 基于 Rust 开发,有 Rust 内存安全和并发的语言优势。
Since its creation in 2012, Servo has contributed to W3C and WHATWG web standards, reporting specification issues and submitting new cross-browser automated tests, and core team members have co-edited new standards that have been adopted by other browsers. As a result, the Servo project helps drive the entire web platform forward, while building on a platform of reusable, modular technologies that implement web standards.
自 2012 年开始,Servo 已经贡献过 W3C 和 WHATWG web 标准,包括反馈 issue,提交跨浏览器自动化测试,并且核心团队共同编辑过新标准并被其他浏览器采纳。Servo 帮助驱动了整个 web 平台发展。
LOL HTML 采用了 Servo 项目提供的 cssparsers 和 selectors 实现了 CSS Selector 的解析和匹配能力。
但是 Servo 提供的 selectors 并不能很好满足边缘和 rewriter 的需求。
- lol-html 有双 Paser 架构
- rewriters 和浏览器场景不一样,rewriters 面临的是多 tag 少 selector 的场景,不适合浏览器自右向左匹配的方式。(补充阅读:Why do browsers match CSS selectors from right to left?)
Selector VM
selector: body > div span
VM 代码
0| expect <body>
1| expect <div>
2| hereditary_jmp 3
3| expect <span>
4| match
输入:<body><div><a><div>
输入 <span>
- 执行 vm 第一行
<expect <body>>失败 - 寻找 stack 上的
hereditary_jmp运行expect <span>成功
成功命中
输入: <span> 重复以上逻辑再次命中
输入:</span></span></div></a></div>,栈弹出。
性能优化
在边缘场景 CPU 和 Mem 极为紧张,需要非常机制的优化。
内存优化
MemoryLimiter
在整个程序生命周期中对内存进行限制不能超过预设的 max 值。
#[derive(Debug)]
pub struct MemoryLimiter {
current_usage: usize,
max: usize,
}
统计逻辑
- increase_usage: 所有内存分配前需要记录
- preallocate:所有预分配内存前需要记录,如果超过直接 panic
- decrease_usage:所有内存释放后需要记录
#[inline]
pub fn increase_usage(&mut self, byte_count: usize) -> Result<(), MemoryLimitExceededError> {
self.current_usage += byte_count;
if self.current_usage > self.max {
Err(MemoryLimitExceededError)
} else {
Ok(())
}
}
#[inline]
pub fn preallocate(&mut self, byte_count: usize) {
self.increase_usage(byte_count).expect(
"Total preallocated memory size should be less than `MemorySettings::max_allowed_memory_usage`.",
);
}
#[inline]
pub fn decrease_usage(&mut self, byte_count: usize) {
self.current_usage -= byte_count;
}
Arena
使用一个 Vec<u8> 预先请求好一片内存,减少运行过程内存请求的次数。并且整个生命周期不会释放。
pub struct Arena {
limiter: SharedMemoryLimiter,
data: Vec<u8>,
}
impl Arena {
pub fn new(limiter: SharedMemoryLimiter, preallocated_size: usize) -> Self {
limiter.borrow_mut().preallocate(preallocated_size);
Arena {
limiter,
// 预先请求
data: Vec::with_capacity(preallocated_size),
}
}
}
在超出预分配内存时,尽量少请求内存。注意看 17 行,使用了 Vec::reserve_exact 方法,vec 的内存仅会扩容申请的大小,而不是 vec 当前 cap 的两倍。
impl Arena {
pub fn append(&mut self, slice: &[u8]) -> Result<(), MemoryLimitExceededError> {
let new_len = self.data.len() + slice.len();
let capacity = self.data.capacity();
if new_len > capacity {
let additional = new_len - capacity;
// NOTE: approximate usage, as `Vec::reserve_exact` doesn't
// give guarantees about exact capacity value :).
self.limiter.borrow_mut().increase_usage(additional)?;
// NOTE: with wicely choosen preallocated size this branch should be
// executed quite rarely. We can't afford to use double capacity
// strategy used by default (see: https://github.com/rust-lang/rust/blob/bdfd698f37184da42254a03ed466ab1f90e6fb6c/src/liballoc/raw_vec.rs#L424)
// as we'll run out of the space allowance quite quickly.
self.data.reserve_exact(slice.len());
}
self.data.extend_from_slice(slice);
Ok(())
}
}
LimitedVec
不直接使用 Vec 而是 LimitedVec,避免内存超限。不知道这里是不是为了避免 unsafe,这里应该使用 Arena 申请好的内存更加合适。
pub struct LimitedVec<T> {
limiter: SharedMemoryLimiter,
vec: Vec<T>,
}
LocalNameHash
将 Html 中的 tag 压缩为一个 u64 的数字,减少内存占用。
struct LocalNameHash(Option<u64>);
所有标准标签名仅包含ASCII字母字符和1到6的数字(在编号的标题标签中,即<h1> - <h6>)。考虑到标签名不区分大小写,我们只有26 + 6 = 32个字符。因此,单个字符可以用5位来编码,我们可以在64位整数中最多容纳64 / 5 ≈ 12个字符。这足以编码所有标准标签名,所以我们可以仅比较整数而不是昂贵的字符串比较标签名。
这个标签哈希类似物的原始想法属于IngvarStepanyan,并在lazyhtml中实现。因此,向他致敬,因为他想出了这个很酷的优化。这个实现与原始版本不同,因为它增加了编码1到6数字的能力,这使我们能够编码编号的标题标签。
在这个实现中,我们为1到6的数字保留了0到5的数字,而为ASCII字母保留了6到31的数字。否则,如果我们使用0到25的数字来表示ASCII字母,我们将会有一个重复的a字符的歧义:a、aaa甚至aaaaa都会给我们0作为哈希值。这仍然是数字的情况,但考虑到标签名不能以数字开始,我们在这里是安全的,因为我们将只获得通过零向左移位的第一个字符随着重复的1数字被添加到哈希中。
计算逻辑 第 10 行: 判断前 5 位是否未写入过,这时候才是安全的 第 17 行: 对 a-zA-Z 进行编码 第 22 行: 对 1-6 进行编码
impl LocalNameHash {
#[inline]
pub fn update(&mut self, ch: u8) {
if let Some(h) = self.0 {
// NOTE: check if we still have space for yet another
// character and if not then invalidate the hash.
// Note, that we can't have `1` (which is encoded as 0b00000) as
// a first character of a tag name, so it's safe to perform
// check this way.
self.0 = if h >> (64 - 5) == 0 {
match ch {
// NOTE: apply 0x1F mask on ASCII alpha to convert it to the
// number from 1 to 26 (character case is controlled by one of
// upper bits which we eliminate with the mask). Then add
// 5, since numbers from 0 to 5 are reserved for digits.
// Aftwerards put result as 5 lower bits of the hash.
b'a'..=b'z' | b'A'..=b'Z' => Some((h << 5) | ((u64::from(ch) & 0x1F) + 5)),
// NOTE: apply 0x0F mask on ASCII digit to convert it to number
// from 1 to 6. Then substract 1 to make it zero-based.
// Afterwards, put result as lower bits of the hash.
b'1'..=b'6' => Some((h << 5) | ((u64::from(ch) & 0x0F) - 1)),
// NOTE: for any other characters hash function is not
// applicable, so we completely invalidate the hash.
_ => None,
}
} else {
None
};
}
}
}
计算优化
worker 中最慢的操作来自于 native 和 JS 交互的部分,原因是需要做 JS 对象管理、分配内存、序列化和反序列化。
因此 LOL HTML 采用了短路的方式,如果不需要 JS 部分判断,直接响应走,不再 JS 中绕一圈。
工程优化
宏
宏的:
- declarative macros: 通过
macro_rules!定义语法扩展 - procedural macros:
- Custom #[derive] macros that specify code added with the derive attribute used on structs and enums
- Attribute-like macros that define custom attributes usable on any item
- Function-like macros that look like function calls but operate on the tokens specified as their argument
declarative macro
$branches 这里会替换为 children 和 descendants。ident 是宏里的一种语法,所有语法可以查看 Reference。
pub fn add_selector(&mut self, selector: &Selector, payload: P) {
macro_rules! host_and_switch_branch_vec {
($branches:ident) => {{
let node_idx = Self::host_expressions(
predicate,
branches,
&mut self.cumulative_node_count,
);
branches = &mut branches[node_idx].$branches;
predicate = Predicate::default();
}};
}
Component::Combinator(c) => match c {
Combinator::Child => host_and_switch_branch_vec!(children),
Combinator::Descendant => host_and_switch_branch_vec!(descendants),
...
}
}
cfg
通过 cfg(feature) 可以在编译期间选择开启关闭功能
cfg_if! {
if #[cfg(feature = "debug_trace")] {
macro_rules! trace {
( @write $slice:expr ) => {
use crate::base::Bytes;
println!("-- Write: {:#?}", Bytes::from($slice));
};
}
} else {
macro_rules! trace {
( @$ty:ident $($args:tt)* ) => {};
( @$ty:ident $($args:tt),* ) => {};
}
}
}
pub fn write(&mut self, data: &[u8]) -> Result<(), RewritingError> {
trace!(@write data);
...
}
添加 --features=debug_trace 可以进行 debug。
cat examples/defer_scripts/index.html | cargo run --features=debug_trace --example=defer_scripts
DSL
HTML 的标准比较复杂,LOL HTML 基于宏自己定义了一套描述 HTML 状态机的 DSL。
#[macro_use]
mod attributes;
define_state_group!(tag_states_group = {
tag_open_state {
b'!' => ( unmark_tag_start; --> markup_declaration_open_state )
b'/' => ( --> end_tag_open_state )
alpha => ( create_start_tag; start_token_part; update_tag_name_hash; --> tag_name_state )
b'?' => ( unmark_tag_start; create_comment; start_token_part; --> bogus_comment_state )
eof => ( emit_text?; emit_eof?; )
_ => ( unmark_tag_start; emit_text?; reconsume in data_state )
}
});
本章作业
rust wasm 入门 rustwasm.github.io/docs/book/i… 构建 js-api 为 wasm 产物,在 service-worker 中使用,为 script 添加 defer attribute。