perf(dom): better inline script performance

This commit is contained in:
ldm0
2026-10-05 20:14:33 +08:00
parent 7909cff7b6
commit b75de5c66f
6 changed files with 163 additions and 36 deletions
+41 -10
View File
@@ -564,6 +564,46 @@ impl DomHost {
self.set_text_content_effects(handle, value).did_change()
}
/// Appends parser character tokens while retaining the text node's capacity.
/// Old text is copied only when mutation records need its previous value.
pub fn append_text_to_text_node_effects(
&mut self,
handle: DomHandle,
value: &str,
) -> DomMutationEffects {
if value.is_empty() {
return DomMutationEffects::default();
}
let records_enabled = self.mutation_records_enabled();
let Some(text) = self
.node_mut(handle)
.and_then(|node| node.data_mut().as_text_mut())
else {
return DomMutationEffects::default();
};
let old_value = records_enabled.then(|| text.data().to_owned());
text.append_data(value);
self.text_data_mutation_effects(handle, records_enabled, old_value)
}
fn text_data_mutation_effects(
&mut self,
handle: DomHandle,
records_enabled: bool,
old_value: Option<String>,
) -> DomMutationEffects {
self.record_mutation(MutationScope::QueryState);
let mut effects = self.node_update_effects(handle);
effects.mark_style_character_data_mutation(handle);
if let Some(parent) = self.parent_node(handle) {
self.mark_stylesheet_owner_contents_change_for_parent(&mut effects, parent);
}
if records_enabled {
effects.mark_character_data_mutation(handle, old_value);
}
effects
}
pub fn set_text_content_effects(
&mut self,
handle: DomHandle,
@@ -617,16 +657,7 @@ impl DomHost {
if !changed {
return DomMutationEffects::default();
}
self.record_mutation(MutationScope::QueryState);
let mut effects = self.node_update_effects(handle);
effects.mark_style_character_data_mutation(handle);
if let Some(parent) = self.parent_node(handle) {
self.mark_stylesheet_owner_contents_change_for_parent(&mut effects, parent);
}
if records_enabled {
effects.mark_character_data_mutation(handle, old_value);
}
effects
self.text_data_mutation_effects(handle, records_enabled, old_value)
}
NodeType::Element | NodeType::DocumentFragment => {
let children = self.child_handles(handle).collect::<Vec<_>>();
+70 -1
View File
@@ -915,6 +915,74 @@ mod tests {
assert!(effects.observer_records().records().is_empty());
}
#[test]
fn appending_text_preserves_replacement_mutation_effects() {
for parent_name in ["p", "style", "script"] {
for recording in [false, true] {
let mut host = DomHost::from_dom(NativeDom::new_html(test_url()));
host.reset_html_document_shell();
let body = host.document_body_handle().expect("document body");
let parent = host.create_element(parent_name);
let text = host.create_text_node("before🙂");
assert!(host.append_child(body, parent));
assert!(host.append_child(parent, text));
host.set_mutation_observer_records_enabled(recording);
let mut replacement = host.clone();
let effects = host.append_text_to_text_node_effects(text, "追加\n");
let expected = replacement.set_text_content_effects(text, "before🙂追加\n");
assert_eq!(
effects, expected,
"parent={parent_name}, recording={recording}"
);
assert_eq!(host.text_content(text).as_deref(), Some("before🙂追加\n"));
assert_eq!(host.query_version(), replacement.query_version());
}
}
}
#[test]
fn appending_text_keeps_existing_dom_snapshots_unchanged() {
let mut host = DomHost::from_dom(NativeDom::new_html(test_url()));
let text = host.create_text_node("before🙂");
let snapshot = host.snapshot_document();
assert!(
host.append_text_to_text_node_effects(text, "追加")
.did_change()
);
assert_eq!(host.text_content(text).as_deref(), Some("before🙂追加"));
assert_eq!(snapshot.text_content(text).as_deref(), Some("before🙂"));
}
#[test]
fn appending_empty_text_or_non_text_nodes_has_no_mutation_effects() {
let mut host = DomHost::from_dom(NativeDom::new_html(test_url()));
let text = host.create_text_node("original");
let element = host.create_element("div");
let document = host.document_handle();
let cdata = host.create_cdata_section_for_document(document, "cdata");
host.set_mutation_observer_records_enabled(true);
let version = host.query_version();
assert_eq!(
host.append_text_to_text_node_effects(text, ""),
DomMutationEffects::default()
);
for handle in [element, cdata, NativeNodeId::new(99_999)] {
assert_eq!(
host.append_text_to_text_node_effects(handle, "ignored"),
DomMutationEffects::default()
);
}
assert_eq!(host.query_version(), version);
assert_eq!(host.text_content(text).as_deref(), Some("original"));
assert_eq!(host.text_content(element).as_deref(), Some(""));
assert_eq!(host.text_content(cdata).as_deref(), Some("cdata"));
}
#[test]
fn insert_before_self_reference_reports_remove_and_reinsert() {
let mut host = DomHost::from_dom(NativeDom::new_html(test_url()));
@@ -957,7 +1025,8 @@ mod tests {
"DocumentType grew to {} bytes",
size_of::<DocumentType>()
);
assert_eq!(size_of::<Text>(), 16);
// Growing text fits within the existing NodeData/Node size limits below.
assert_eq!(size_of::<Text>(), size_of::<String>());
assert_eq!(size_of::<CDataSection>(), 16);
assert_eq!(size_of::<Comment>(), 16);
assert_eq!(size_of::<ProcessingInstruction>(), 32);
+10 -5
View File
@@ -1,13 +1,14 @@
#[derive(Debug, Clone)]
pub struct Text {
data: Box<str>,
// Text shares the larger NodeData enum storage with elements, so retaining
// String capacity does not enlarge a Node. Parser character tokens can then
// append without reallocating and copying the full text for every token.
data: String,
}
impl Text {
pub fn new(data: String) -> Self {
Self {
data: data.into_boxed_str(),
}
Self { data }
}
pub fn data(&self) -> &str {
@@ -15,7 +16,11 @@ impl Text {
}
pub fn set_data(&mut self, data: impl Into<String>) {
self.data = data.into().into_boxed_str();
self.data = data.into();
}
pub(crate) fn append_data(&mut self, data: &str) {
self.data.push_str(data);
}
}
+1 -6
View File
@@ -1637,12 +1637,7 @@ fn append_text_to_text_node_in_host(
node_id: NativeNodeId,
text: String,
) -> DomMutationEffects {
let Some(previous) = host.node(node_id).and_then(Node::as_text) else {
return DomMutationEffects::default();
};
let mut merged = previous.data().to_owned();
merged.push_str(&text);
host.set_text_content_effects(node_id, &merged)
host.append_text_to_text_node_effects(node_id, &text)
}
#[cfg(test)]
+39 -7
View File
@@ -1158,13 +1158,7 @@ mod tests {
text: String,
) -> DomMutationEffects {
// SAFETY: the test keeps the DomHost alive for this parser pump step.
let host = unsafe { &mut *self.host };
let Some(previous) = host.node(node_id).and_then(Node::as_text) else {
return DomMutationEffects::default();
};
let mut merged = previous.data().to_owned();
merged.push_str(&text);
host.set_text_content_effects(node_id, &merged)
unsafe { &mut *self.host }.append_text_to_text_node_effects(node_id, &text)
}
fn push_parse_error(&mut self, error: String) {
@@ -1328,6 +1322,44 @@ mod tests {
}
}
#[test]
fn parser_stream_preserves_large_script_text_across_input_chunks() {
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
Url::parse("https://example.test/page.html").expect("test url"),
);
let block = "{\"text\":\"中文🙂 <node> & tail\"}\n".repeat(256);
let source = block.repeat(128);
assert!(source.len() > 1_000_000);
stream.feed("<script type='application/json'>");
for _ in 0..128 {
stream.feed(&block);
}
stream.feed("</script><p>after script</p>");
let document = stream.finish();
let script = document.elements_by_tag_name(document.document_node_id(), "script", false)[0];
assert_eq!(document.child_ids(script).count(), 1);
assert_eq!(
document.text_content(script).as_deref(),
Some(source.as_str())
);
assert_eq!(
document
.node(script)
.and_then(Node::as_element)
.expect("script element")
.script_text_internal_slot(),
source
);
assert_eq!(
document
.elements_by_tag_name(document.document_node_id(), "p", false)
.len(),
1
);
}
#[test]
fn parser_stream_feed_continues_past_definitive_encoding_indicator() {
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
@@ -382,13 +382,8 @@ impl DocumentRuntime {
node_id: DomHandle,
text: String,
) -> DomMutationEffects {
let host = self.dom_host_mut_for_active_parser_step();
let Some(previous) = host.node(node_id).and_then(Node::as_text) else {
return DomMutationEffects::default();
};
let mut merged = previous.data().to_owned();
merged.push_str(&text);
host.set_text_content_effects(node_id, &merged)
self.dom_host_mut_for_active_parser_step()
.append_text_to_text_node_effects(node_id, &text)
}
pub(crate) fn push_parse_error_in_live_dom_host(&mut self, error: String) {