mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 00:00:56 +00:00
perf(css): defer font projections until collections are exposed
Carry owner refresh intent through CSS synchronization and prepare font projections only after finding an existing FontFaceSet in its creation realm. Native CSS updates continue normally and held collections update without rereading document.fonts. A test-only counter verifies 64 unobserved stylesheet mutations prepare no font projections, first observation prepares current state once, and later mutations and removals update held collections. Validation: cargo fmt --all; cargo clippy --workspace --all-targets --all-features -- -D warnings; cargo nextest run --no-fail-fast (18833 passed, 16 skipped on the final change set).
This commit is contained in:
@@ -129,6 +129,8 @@ pub(super) use construction::{
|
||||
};
|
||||
pub(crate) use cookies::{document_cookie_for_receiver, set_document_cookie_for_receiver};
|
||||
pub(crate) use css_state::install_adopted_style_sheets_array_primordials;
|
||||
#[cfg(test)]
|
||||
pub(crate) use css_state::take_owner_font_face_projection_count_for_test;
|
||||
pub(in crate::native_bridge) use css_state::{
|
||||
AdoptedStyleSheetsArrayOwner, install_adopted_style_sheets_array_mutation_methods,
|
||||
normalize_adopted_style_sheets_assignment,
|
||||
|
||||
@@ -3,6 +3,8 @@ mod font_faces;
|
||||
mod projection;
|
||||
mod shared;
|
||||
mod style_sheets;
|
||||
#[cfg(test)]
|
||||
pub(crate) use font_faces::take_owner_font_face_projection_count_for_test;
|
||||
|
||||
pub(super) use accessors::detached_document_fonts_getter;
|
||||
pub(in crate::native_bridge) use accessors::{
|
||||
|
||||
@@ -23,6 +23,16 @@ const FONT_FACE_STYLESHEET_RULE_IDENTITY_SLOT: &str = "__moliFontFaceStylesheetR
|
||||
const FONT_FACE_STYLESHEET_RULE_FINGERPRINT_SLOT: &str = "__moliFontFaceStylesheetRuleFingerprint";
|
||||
const FONT_FACE_STYLESHEET_ID_SLOT: &str = "__moliFontFaceStylesheetId";
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
static OWNER_FONT_FACE_PROJECTION_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn take_owner_font_face_projection_count_for_test() -> usize {
|
||||
OWNER_FONT_FACE_PROJECTION_COUNT.with(|count| count.replace(0))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) enum OwnerFontFaceProjection {
|
||||
Descriptors(std::sync::Arc<[StylesheetFontFaceDescriptor]>),
|
||||
@@ -36,6 +46,8 @@ pub(super) fn owner_font_face_projection(
|
||||
host: &JsContextHost,
|
||||
owner: DomHandle,
|
||||
) -> Option<OwnerFontFaceProjection> {
|
||||
#[cfg(test)]
|
||||
OWNER_FONT_FACE_PROJECTION_COUNT.with(|count| count.set(count.get() + 1));
|
||||
let dom_host = host.dom_host();
|
||||
let element = dom_host.node(owner)?.as_element()?;
|
||||
if !dom_host.is_connected(owner)
|
||||
@@ -144,13 +156,19 @@ pub(super) fn sync_document_fonts<'s>(
|
||||
pub(super) fn apply_font_face_owner_projection<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
holder: v8::Local<'s, v8::Object>,
|
||||
host: &JsContextHost,
|
||||
owner: DomHandle,
|
||||
projection: Option<&OwnerFontFaceProjection>,
|
||||
refresh: bool,
|
||||
) -> bool {
|
||||
let Some(fonts) = object_property_as_object(scope, holder, FONTS_SLOT) else {
|
||||
return false;
|
||||
};
|
||||
set_owner_font_face_contribution(scope, fonts, owner, projection);
|
||||
// Native CSS preparation and resource discovery have already run. Only
|
||||
// construct the JS collection's projection when that collection exists.
|
||||
let projection = refresh
|
||||
.then(|| owner_font_face_projection(host, owner))
|
||||
.flatten();
|
||||
set_owner_font_face_contribution(scope, fonts, owner, projection.as_ref());
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
use super::{
|
||||
font_faces::{
|
||||
OwnerFontFaceProjection, apply_font_face_owner_projection,
|
||||
finish_font_face_owner_projections, owner_font_face_projection,
|
||||
},
|
||||
font_faces::{apply_font_face_owner_projection, finish_font_face_owner_projections},
|
||||
style_sheets::sync_document_style_sheets,
|
||||
};
|
||||
use crate::{
|
||||
@@ -21,7 +18,7 @@ enum DocumentCssProjection {
|
||||
FontFaceOwner {
|
||||
document: DomHandle,
|
||||
owner: DomHandle,
|
||||
projection: Option<OwnerFontFaceProjection>,
|
||||
refresh: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -41,18 +38,18 @@ impl DocumentCssProjections {
|
||||
let owner = change.owner();
|
||||
let scopes = change.tree_scopes();
|
||||
if let Some(old_scope) = scopes.old() {
|
||||
projections.push_for_scope(host, old_scope, owner, None);
|
||||
projections.push_for_scope(host, old_scope, owner, false);
|
||||
}
|
||||
let current_projection = match change.kind() {
|
||||
let refresh = !matches!(
|
||||
change.kind(),
|
||||
DomStylesheetOwnerChangeKind::TreeConnectionChanged { connected: false }
|
||||
| DomStylesheetOwnerChangeKind::Unregistered => None,
|
||||
_ => owner_font_face_projection(host, owner),
|
||||
};
|
||||
| DomStylesheetOwnerChangeKind::Unregistered
|
||||
);
|
||||
if let Some(current_scope) = scopes.current_scope() {
|
||||
projections.push_for_scope(host, current_scope, owner, current_projection.clone());
|
||||
projections.push_for_scope(host, current_scope, owner, refresh);
|
||||
}
|
||||
if let Some(new_scope) = scopes.new_scope() {
|
||||
projections.push_for_scope(host, new_scope, owner, current_projection.clone());
|
||||
projections.push_for_scope(host, new_scope, owner, refresh);
|
||||
}
|
||||
}
|
||||
projections
|
||||
@@ -63,8 +60,7 @@ impl DocumentCssProjections {
|
||||
let Some(tree_scope) = host.dom_host().root_node_handle(owner) else {
|
||||
return projections;
|
||||
};
|
||||
let projection = owner_font_face_projection(host, owner);
|
||||
projections.push_for_scope(host, tree_scope, owner, projection);
|
||||
projections.push_for_scope(host, tree_scope, owner, true);
|
||||
projections
|
||||
}
|
||||
|
||||
@@ -73,7 +69,7 @@ impl DocumentCssProjections {
|
||||
host: &JsContextHost,
|
||||
scope: DomHandle,
|
||||
owner: DomHandle,
|
||||
projection: Option<OwnerFontFaceProjection>,
|
||||
refresh: bool,
|
||||
) {
|
||||
if !host.dom_host().node(scope).is_some_and(Node::is_document) {
|
||||
return;
|
||||
@@ -94,20 +90,20 @@ impl DocumentCssProjections {
|
||||
{
|
||||
let existing = &mut self.projections[index];
|
||||
let DocumentCssProjection::FontFaceOwner {
|
||||
projection: existing_projection,
|
||||
refresh: existing_refresh,
|
||||
..
|
||||
} = existing
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
*existing_projection = projection;
|
||||
*existing_refresh = refresh;
|
||||
return;
|
||||
}
|
||||
let index = self.projections.len();
|
||||
self.projections.push(DocumentCssProjection::FontFaceOwner {
|
||||
document: scope,
|
||||
owner,
|
||||
projection,
|
||||
refresh,
|
||||
});
|
||||
self.font_face_projection_indices
|
||||
.insert((scope, owner), index);
|
||||
@@ -133,14 +129,10 @@ impl DocumentCssProjections {
|
||||
DocumentCssProjection::FontFaceOwner {
|
||||
document,
|
||||
owner,
|
||||
projection,
|
||||
refresh,
|
||||
} => {
|
||||
if apply_font_face_owner_projection(
|
||||
scope,
|
||||
holder,
|
||||
owner,
|
||||
projection.as_ref(),
|
||||
) && !font_face_documents.contains(&document)
|
||||
if apply_font_face_owner_projection(scope, holder, host, owner, refresh)
|
||||
&& !font_face_documents.contains(&document)
|
||||
{
|
||||
font_face_documents.push(document);
|
||||
}
|
||||
|
||||
@@ -1323,3 +1323,61 @@ fn inline_stylesheet_copy_on_write_preserves_unmodified_font_face_wrappers() {
|
||||
r#"{"initialCount":2,"insertPreservedFirst":true,"insertPreservedSecond":true,"descriptorReplacedFirst":true,"descriptorPreservedSecond":true}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn css_owner_changes_prepare_font_projections_only_for_exposed_collections() {
|
||||
use crate::native_bridge::document::take_owner_font_face_projection_count_for_test;
|
||||
let mut vm = new_storage_test_vm("https://lazy-font-projection.test/");
|
||||
take_owner_font_face_projection_count_for_test();
|
||||
vm.eval(
|
||||
r#"
|
||||
globalThis.lazyStyle = document.createElement('style');
|
||||
(document.head || document.documentElement || document).appendChild(lazyStyle);
|
||||
globalThis.heldSheets = document.styleSheets;
|
||||
for (let i = 0; i < 64; i++) {
|
||||
lazyStyle.textContent = `@font-face { font-family: Face${i}; src: local(Face${i}); }
|
||||
#styled { color: rgb(1, 2, 3); }`;
|
||||
}
|
||||
globalThis.styled = document.createElement('div'); styled.id = 'styled';
|
||||
(document.body || document.documentElement || document).appendChild(styled);
|
||||
'ready'
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
take_owner_font_face_projection_count_for_test(),
|
||||
0,
|
||||
"unobserved FontFaceSets must not prepare owner projections"
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval("[heldSheets.length, getComputedStyle(styled).color].join('|')")
|
||||
.unwrap(),
|
||||
"1|rgb(1, 2, 3)"
|
||||
);
|
||||
assert_eq!(
|
||||
take_owner_font_face_projection_count_for_test(),
|
||||
0,
|
||||
"native styling and exposed StyleSheetLists do not consume JS font projections"
|
||||
);
|
||||
assert_eq!(vm.eval("globalThis.heldFonts = document.fonts; Array.from(heldFonts, face => face.family).join(',')").unwrap(), "Face63");
|
||||
assert_eq!(
|
||||
take_owner_font_face_projection_count_for_test(),
|
||||
1,
|
||||
"first observation prepares the current native state once"
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"
|
||||
lazyStyle.textContent = '@font-face { font-family: UpdatedFace; src: local(UpdatedFace); }';
|
||||
Array.from(heldFonts, face => face.family).join(',')
|
||||
"#
|
||||
)
|
||||
.unwrap(),
|
||||
"UpdatedFace"
|
||||
);
|
||||
assert!(
|
||||
take_owner_font_face_projection_count_for_test() > 0,
|
||||
"already held FontFaceSets must update without rereading Document.fonts"
|
||||
);
|
||||
assert_eq!(vm.eval("lazyStyle.remove(); [heldFonts.size, heldSheets.length, heldFonts === document.fonts].join('|')").unwrap(), "0|0|true");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user