we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement style sharing cache for cascade resolution

Adds a StyleCache that reuses computed styles across elements with
identical styling inputs (matched rules, inline style, parent style),
avoiding redundant cascade resolution. Cache keys are computed from
hashed matched rule sets, inline style attribute strings, and parent
style IDs. The cache is scoped per style resolution pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+307 -32
+307 -32
crates/style/src/computed.rs
··· 5 5 //! handling property inheritance, and resolving relative values. 6 6 7 7 use std::collections::HashMap; 8 + use std::hash::{Hash, Hasher}; 8 9 9 10 use we_css::animations::{ 10 11 parse_animation_delay, parse_animation_direction, parse_animation_duration, ··· 22 23 use we_dom::{Document, NodeData, NodeId}; 23 24 24 25 use crate::matching::collect_matching_rules; 26 + 27 + // --------------------------------------------------------------------------- 28 + // Style sharing cache 29 + // --------------------------------------------------------------------------- 30 + 31 + /// Key for the style sharing cache. 32 + /// 33 + /// Two elements with the same matched rules, inline style, and parent computed 34 + /// style will always produce identical computed styles — the cascade is 35 + /// deterministic given these inputs. 36 + #[derive(Debug, Clone, PartialEq, Eq, Hash)] 37 + struct StyleCacheKey { 38 + /// Hash of the sorted matched rule set (source order + specificity). 39 + matched_rules_hash: u64, 40 + /// Hash of the raw inline `style` attribute string. 41 + inline_style_hash: u64, 42 + /// Identifier of the parent's computed style in the cache. 43 + parent_style_id: u64, 44 + } 45 + 46 + /// Cache that reuses computed styles across elements with identical styling 47 + /// inputs, avoiding redundant cascade resolution. 48 + pub struct StyleCache { 49 + entries: HashMap<StyleCacheKey, (ComputedStyle, u64)>, 50 + next_id: u64, 51 + /// Number of cache hits during this resolution pass. 52 + pub hits: usize, 53 + /// Number of cache misses during this resolution pass. 54 + pub misses: usize, 55 + } 56 + 57 + impl StyleCache { 58 + fn new() -> Self { 59 + Self { 60 + entries: HashMap::new(), 61 + // ID 0 is reserved for the root parent style (ComputedStyle::default()). 62 + next_id: 1, 63 + hits: 0, 64 + misses: 0, 65 + } 66 + } 67 + } 68 + 69 + /// Compute a 64-bit hash of the matched rule set. 70 + fn hash_matched_rules(rules: &[crate::matching::MatchedRule<'_>]) -> u64 { 71 + let mut hasher = std::collections::hash_map::DefaultHasher::new(); 72 + for r in rules { 73 + r.source_order.hash(&mut hasher); 74 + r.specificity.a.hash(&mut hasher); 75 + r.specificity.b.hash(&mut hasher); 76 + r.specificity.c.hash(&mut hasher); 77 + } 78 + hasher.finish() 79 + } 80 + 81 + /// Compute a 64-bit hash of an optional inline style attribute string. 82 + fn hash_inline_style(attr: Option<&str>) -> u64 { 83 + let mut hasher = std::collections::hash_map::DefaultHasher::new(); 84 + attr.hash(&mut hasher); 85 + hasher.finish() 86 + } 25 87 26 88 // --------------------------------------------------------------------------- 27 89 // Display ··· 2498 2560 author_stylesheets: &[Stylesheet], 2499 2561 media_ctx: &MediaContext, 2500 2562 ) -> Option<StyledNode> { 2563 + let (result, _cache) = resolve_styles_with_cache(doc, author_stylesheets, media_ctx); 2564 + result 2565 + } 2566 + 2567 + /// Resolve styles and return the style cache for inspection (e.g. hit/miss 2568 + /// statistics). This is the core entry point used by the other `resolve_*` 2569 + /// functions. 2570 + pub fn resolve_styles_with_cache( 2571 + doc: &Document, 2572 + author_stylesheets: &[Stylesheet], 2573 + media_ctx: &MediaContext, 2574 + ) -> (Option<StyledNode>, StyleCache) { 2501 2575 let ua = ua_stylesheet(); 2502 2576 2503 2577 // Combine UA + author stylesheets into a single list for rule collection. ··· 2511 2585 2512 2586 let viewport = media_ctx.viewport(); 2513 2587 let root = doc.root(); 2514 - resolve_node( 2588 + let mut cache = StyleCache::new(); 2589 + let result = resolve_node( 2515 2590 doc, 2516 2591 root, 2517 2592 &combined, 2518 2593 &ComputedStyle::default(), 2594 + 0, // root parent style ID 2519 2595 viewport, 2520 2596 media_ctx, 2521 - ) 2597 + &mut cache, 2598 + ); 2599 + (result, cache) 2522 2600 } 2523 2601 2602 + #[allow(clippy::too_many_arguments)] 2524 2603 fn resolve_node( 2525 2604 doc: &Document, 2526 2605 node: NodeId, 2527 2606 stylesheet: &Stylesheet, 2528 2607 parent_style: &ComputedStyle, 2608 + parent_style_id: u64, 2529 2609 viewport: (f32, f32), 2530 2610 media_ctx: &MediaContext, 2611 + cache: &mut StyleCache, 2531 2612 ) -> Option<StyledNode> { 2532 2613 match doc.node_data(node) { 2533 2614 NodeData::Document => { 2534 2615 // Document node: resolve children, return first element child or wrapper. 2535 2616 let mut children = Vec::new(); 2536 2617 for child in doc.children(node) { 2537 - if let Some(styled) = 2538 - resolve_node(doc, child, stylesheet, parent_style, viewport, media_ctx) 2539 - { 2618 + if let Some(styled) = resolve_node( 2619 + doc, 2620 + child, 2621 + stylesheet, 2622 + parent_style, 2623 + parent_style_id, 2624 + viewport, 2625 + media_ctx, 2626 + cache, 2627 + ) { 2540 2628 children.push(styled); 2541 2629 } 2542 2630 } ··· 2557 2645 ref tag_name, 2558 2646 .. 2559 2647 } => { 2560 - let style = 2561 - compute_style_for_element(doc, node, stylesheet, parent_style, viewport, media_ctx); 2648 + // Step 1: Collect inputs needed for cache key. 2649 + let matched_rules = collect_matching_rules(doc, node, stylesheet, media_ctx); 2650 + let inline_style_attr = doc.get_attribute(node, "style").map(|s| s.to_owned()); 2651 + 2652 + let key = StyleCacheKey { 2653 + matched_rules_hash: hash_matched_rules(&matched_rules), 2654 + inline_style_hash: hash_inline_style(inline_style_attr.as_deref()), 2655 + parent_style_id, 2656 + }; 2657 + 2658 + // Step 2: Check cache before running the cascade. 2659 + let (style, style_id) = if let Some((cached, id)) = cache.entries.get(&key) { 2660 + cache.hits += 1; 2661 + (cached.clone(), *id) 2662 + } else { 2663 + cache.misses += 1; 2664 + let inline_decls = parse_inline_style_from_attr(inline_style_attr.as_deref()); 2665 + let style = compute_style_for_element_with_inputs( 2666 + &matched_rules, 2667 + &inline_decls, 2668 + parent_style, 2669 + viewport, 2670 + ); 2671 + let id = cache.next_id; 2672 + cache.next_id += 1; 2673 + cache.entries.insert(key, (style.clone(), id)); 2674 + (style, id) 2675 + }; 2562 2676 2563 2677 if style.display == Display::None { 2564 2678 return None; ··· 2572 2686 let mut children = Vec::new(); 2573 2687 if !is_svg_root { 2574 2688 for child in doc.children(node) { 2575 - if let Some(styled) = 2576 - resolve_node(doc, child, stylesheet, &style, viewport, media_ctx) 2577 - { 2689 + if let Some(styled) = resolve_node( 2690 + doc, child, stylesheet, &style, style_id, viewport, media_ctx, cache, 2691 + ) { 2578 2692 children.push(styled); 2579 2693 } 2580 2694 } ··· 2601 2715 } 2602 2716 } 2603 2717 2604 - /// Compute the style for a single element node. 2605 - fn compute_style_for_element( 2606 - doc: &Document, 2607 - node: NodeId, 2608 - stylesheet: &Stylesheet, 2718 + /// Compute the style for a single element given pre-collected matched rules 2719 + /// and inline style declarations. This is the core cascade function, called 2720 + /// on cache miss. 2721 + fn compute_style_for_element_with_inputs( 2722 + matched_rules: &[crate::matching::MatchedRule<'_>], 2723 + inline_decls: &[Declaration], 2609 2724 parent_style: &ComputedStyle, 2610 2725 viewport: (f32, f32), 2611 - media_ctx: &MediaContext, 2612 2726 ) -> ComputedStyle { 2613 2727 // Start from initial values, inheriting inherited properties from parent 2614 2728 let mut style = ComputedStyle { ··· 2624 2738 custom_properties: parent_style.custom_properties.clone(), 2625 2739 ..ComputedStyle::default() 2626 2740 }; 2627 - 2628 - // Step 2: Collect matching rules, sorted by specificity + source order 2629 - let matched_rules = collect_matching_rules(doc, node, stylesheet, media_ctx); 2630 - 2631 - // Step 3: Parse inline style declarations (from style attribute). 2632 - let inline_decls = parse_inline_style(doc, node); 2633 2741 2634 2742 // ---- Pass 1: Resolve custom properties in cascade order ---- 2635 2743 // Normal declarations from stylesheets 2636 - for matched in &matched_rules { 2744 + for matched in matched_rules { 2637 2745 for decl in &matched.rule.declarations { 2638 2746 if decl.property.starts_with("--") && !decl.important { 2639 2747 apply_custom_property( ··· 2646 2754 } 2647 2755 } 2648 2756 // Normal inline declarations 2649 - for decl in &inline_decls { 2757 + for decl in inline_decls { 2650 2758 if decl.property.starts_with("--") && !decl.important { 2651 2759 apply_custom_property( 2652 2760 &mut style.custom_properties, ··· 2657 2765 } 2658 2766 } 2659 2767 // !important declarations from stylesheets 2660 - for matched in &matched_rules { 2768 + for matched in matched_rules { 2661 2769 for decl in &matched.rule.declarations { 2662 2770 if decl.property.starts_with("--") && decl.important { 2663 2771 apply_custom_property( ··· 2670 2778 } 2671 2779 } 2672 2780 // !important inline declarations 2673 - for decl in &inline_decls { 2781 + for decl in inline_decls { 2674 2782 if decl.property.starts_with("--") && decl.important { 2675 2783 apply_custom_property( 2676 2784 &mut style.custom_properties, ··· 2685 2793 let mut normal_decls: Vec<(String, CssValue)> = Vec::new(); 2686 2794 let mut important_decls: Vec<(String, CssValue)> = Vec::new(); 2687 2795 2688 - for matched in &matched_rules { 2796 + for matched in matched_rules { 2689 2797 for decl in &matched.rule.declarations { 2690 2798 if decl.property.starts_with("--") { 2691 2799 continue; ··· 2741 2849 } 2742 2850 2743 2851 // Step 6: Apply inline style normal declarations (override stylesheet normals) 2744 - for decl in &inline_decls { 2852 + for decl in inline_decls { 2745 2853 if !decl.important && !decl.property.starts_with("--") { 2746 2854 let resolved_values; 2747 2855 let values = if has_var_references(&decl.value) { ··· 2780 2888 } 2781 2889 2782 2890 // Step 8: Apply inline style !important declarations (highest priority) 2783 - for decl in &inline_decls { 2891 + for decl in inline_decls { 2784 2892 if decl.important && !decl.property.starts_with("--") { 2785 2893 let resolved_values; 2786 2894 let values = if has_var_references(&decl.value) { ··· 2816 2924 style 2817 2925 } 2818 2926 2819 - /// Parse inline style from the `style` attribute of an element. 2820 - fn parse_inline_style(doc: &Document, node: NodeId) -> Vec<Declaration> { 2821 - if let Some(style_attr) = doc.get_attribute(node, "style") { 2927 + /// Parse inline style declarations from a raw attribute string. 2928 + fn parse_inline_style_from_attr(style_attr: Option<&str>) -> Vec<Declaration> { 2929 + if let Some(style_attr) = style_attr { 2822 2930 // Wrap in a dummy rule so the CSS parser can parse it 2823 2931 let css = format!("x {{ {style_attr} }}"); 2824 2932 let ss = we_css::parser::Parser::parse(&css); ··· 4469 4577 let div = &styled.children[0].children[0]; 4470 4578 assert_eq!(div.style.row_gap, 10.0); 4471 4579 assert_eq!(div.style.column_gap, 20.0); 4580 + } 4581 + 4582 + // ----------------------------------------------------------------------- 4583 + // Style sharing cache 4584 + // ----------------------------------------------------------------------- 4585 + 4586 + fn resolve_with_cache( 4587 + doc: &Document, 4588 + css: &str, 4589 + viewport: (f32, f32), 4590 + ) -> (Option<StyledNode>, StyleCache) { 4591 + let ss = Parser::parse(css); 4592 + let media_ctx = we_css::media::MediaContext::from_viewport(viewport.0, viewport.1); 4593 + resolve_styles_with_cache(doc, &[ss], &media_ctx) 4594 + } 4595 + 4596 + #[test] 4597 + fn cache_hits_on_identical_siblings() { 4598 + // Multiple <li> elements matching the same rule with the same parent 4599 + // should produce cache hits after the first one. 4600 + let (mut doc, _, _, body) = make_doc_with_body(); 4601 + let ul = doc.create_element("ul"); 4602 + doc.append_child(body, ul); 4603 + for i in 0..5 { 4604 + let li = doc.create_element("li"); 4605 + let text = doc.create_text(&format!("Item {i}")); 4606 + doc.append_child(ul, li); 4607 + doc.append_child(li, text); 4608 + } 4609 + 4610 + let (result, cache) = resolve_with_cache(&doc, "li { color: red; }", (800.0, 600.0)); 4611 + assert!(result.is_some()); 4612 + // First <li> is a miss, remaining 4 are hits. 4613 + assert!(cache.hits >= 4, "expected >= 4 hits, got {}", cache.hits); 4614 + } 4615 + 4616 + #[test] 4617 + fn cache_miss_on_different_classes() { 4618 + // Elements with different classes matching different rules should miss. 4619 + let (mut doc, _, _, body) = make_doc_with_body(); 4620 + let div_a = doc.create_element("div"); 4621 + doc.set_attribute(div_a, "class", "a"); 4622 + let div_b = doc.create_element("div"); 4623 + doc.set_attribute(div_b, "class", "b"); 4624 + let text_a = doc.create_text("A"); 4625 + let text_b = doc.create_text("B"); 4626 + doc.append_child(body, div_a); 4627 + doc.append_child(div_a, text_a); 4628 + doc.append_child(body, div_b); 4629 + doc.append_child(div_b, text_b); 4630 + 4631 + let (_result, cache) = resolve_with_cache( 4632 + &doc, 4633 + ".a { color: red; } .b { color: blue; }", 4634 + (800.0, 600.0), 4635 + ); 4636 + // html, body, div.a, div.b — at least 2 misses for the two divs 4637 + assert!( 4638 + cache.misses >= 2, 4639 + "expected >= 2 misses, got {}", 4640 + cache.misses 4641 + ); 4642 + } 4643 + 4644 + #[test] 4645 + fn cache_hit_on_same_inline_style() { 4646 + // Elements with the same inline style and same matched rules should share. 4647 + let (mut doc, _, _, body) = make_doc_with_body(); 4648 + for _ in 0..3 { 4649 + let p = doc.create_element("p"); 4650 + doc.set_attribute(p, "style", "color: green"); 4651 + let text = doc.create_text("Hi"); 4652 + doc.append_child(body, p); 4653 + doc.append_child(p, text); 4654 + } 4655 + 4656 + let (result, cache) = resolve_with_cache(&doc, "", (800.0, 600.0)); 4657 + assert!(result.is_some()); 4658 + // First <p> is a miss, remaining 2 are hits. 4659 + assert!(cache.hits >= 2, "expected >= 2 hits, got {}", cache.hits); 4660 + } 4661 + 4662 + #[test] 4663 + fn cache_miss_on_different_inline_styles() { 4664 + // Elements with different inline styles should not share. 4665 + let (mut doc, _, _, body) = make_doc_with_body(); 4666 + let p1 = doc.create_element("p"); 4667 + doc.set_attribute(p1, "style", "color: red"); 4668 + let p2 = doc.create_element("p"); 4669 + doc.set_attribute(p2, "style", "color: blue"); 4670 + let t1 = doc.create_text("A"); 4671 + let t2 = doc.create_text("B"); 4672 + doc.append_child(body, p1); 4673 + doc.append_child(p1, t1); 4674 + doc.append_child(body, p2); 4675 + doc.append_child(p2, t2); 4676 + 4677 + let (_result, cache) = resolve_with_cache(&doc, "", (800.0, 600.0)); 4678 + // p1 and p2 should both be misses (different inline styles) 4679 + assert!(cache.misses >= 2); 4680 + } 4681 + 4682 + #[test] 4683 + fn cache_correctness_styles_match() { 4684 + // Verify that cached styles produce the same result as uncached. 4685 + let (mut doc, _, _, body) = make_doc_with_body(); 4686 + let ul = doc.create_element("ul"); 4687 + doc.append_child(body, ul); 4688 + for i in 0..3 { 4689 + let li = doc.create_element("li"); 4690 + let text = doc.create_text(&format!("Item {i}")); 4691 + doc.append_child(ul, li); 4692 + doc.append_child(li, text); 4693 + } 4694 + 4695 + let (result, _) = resolve_with_cache( 4696 + &doc, 4697 + "li { color: red; font-size: 20px; margin: 5px; }", 4698 + (800.0, 600.0), 4699 + ); 4700 + let root = result.unwrap(); 4701 + // Find <ul> -> collect <li> styles 4702 + let ul_node = &root.children[0].children[0]; 4703 + let li_styles: Vec<&ComputedStyle> = ul_node.children.iter().map(|c| &c.style).collect(); 4704 + // All <li> elements should have identical styles. 4705 + for s in &li_styles { 4706 + assert_eq!(s.color, Color::rgb(255, 0, 0)); 4707 + assert_eq!(s.font_size, 20.0); 4708 + assert_eq!(s.margin_top, LengthOrAuto::Length(5.0)); 4709 + } 4710 + } 4711 + 4712 + #[test] 4713 + fn cache_respects_different_parents() { 4714 + // Two identical elements under parents with different inherited styles 4715 + // should NOT share cache entries. 4716 + let (mut doc, _, _, body) = make_doc_with_body(); 4717 + let div_a = doc.create_element("div"); 4718 + doc.set_attribute(div_a, "class", "a"); 4719 + let div_b = doc.create_element("div"); 4720 + doc.set_attribute(div_b, "class", "b"); 4721 + doc.append_child(body, div_a); 4722 + doc.append_child(body, div_b); 4723 + 4724 + let span_a = doc.create_element("span"); 4725 + let text_a = doc.create_text("X"); 4726 + doc.append_child(div_a, span_a); 4727 + doc.append_child(span_a, text_a); 4728 + 4729 + let span_b = doc.create_element("span"); 4730 + let text_b = doc.create_text("X"); 4731 + doc.append_child(div_b, span_b); 4732 + doc.append_child(span_b, text_b); 4733 + 4734 + let (result, _) = resolve_with_cache( 4735 + &doc, 4736 + ".a { color: red; } .b { color: blue; }", 4737 + (800.0, 600.0), 4738 + ); 4739 + let root = result.unwrap(); 4740 + let div_a_node = &root.children[0].children[0]; 4741 + let div_b_node = &root.children[0].children[1]; 4742 + let span_a_node = &div_a_node.children[0]; 4743 + let span_b_node = &div_b_node.children[0]; 4744 + // Spans inherit color from different parents. 4745 + assert_eq!(span_a_node.style.color, Color::rgb(255, 0, 0)); 4746 + assert_eq!(span_b_node.style.color, Color::rgb(0, 0, 255)); 4472 4747 } 4473 4748 }