@@ -14,6 +14,8 @@ const RUNTIME_PLAYBACK_ATTRS = new Set<string>([...PREVIEW_ONLY_ATTRS, CLIP_HIDD
1414let sheet : HTMLStyleElement | null = null ;
1515let observer : MutationObserver | null = null ;
1616let hintRuleSelector : string | null = null ;
17+ let watchingAttributes : boolean | null = null ;
18+ const marked = new Set < Element > ( ) ;
1719const markedWith = new WeakMap < Element , string > ( ) ;
1820
1921// Other hints (opacity, filter) make boundaries of their own, so only a pure transform hint goes.
@@ -53,19 +55,23 @@ function inlineFit(element: Element, style: CSSStyleDeclaration): string {
5355// the layer, so that only happens when the element itself changed.
5456function fits ( element : Element , readOwnHints : boolean ) : boolean {
5557 if ( element === sheet ) return false ;
56- const marked = element . hasAttribute ( PREVIEW_RASTER_ATTR ) ;
57- if ( marked && readOwnHints ) element . removeAttribute ( PREVIEW_RASTER_ATTR ) ;
58+ const isMarked = marked . has ( element ) ;
59+ if ( isMarked && readOwnHints ) setMark ( element , false ) ;
5860 const style = getComputedStyle ( element ) ;
59- const hintsFit = ( marked && ! readOwnHints ) || onlyTransformHints ( style ) ;
61+ const hintsFit = ( isMarked && ! readOwnHints ) || onlyTransformHints ( style ) ;
6062 const drop = hintsFit && canDropHint ( element , style ) ;
6163 if ( drop ) markedWith . set ( element , inlineFit ( element , style ) ) ;
6264 return drop ;
6365}
6466
67+ function setMark ( element : Element , on : boolean ) : void {
68+ element . toggleAttribute ( PREVIEW_RASTER_ATTR , on ) ;
69+ if ( on ) marked . add ( element ) ;
70+ else marked . delete ( element ) ;
71+ }
72+
6573function writeMark ( element : Element , drop : boolean ) : void {
66- if ( drop !== element . hasAttribute ( PREVIEW_RASTER_ATTR ) ) {
67- element . toggleAttribute ( PREVIEW_RASTER_ATTR , drop ) ;
68- }
74+ if ( drop !== marked . has ( element ) ) setMark ( element , drop ) ;
6975}
7076
7177function check ( element : Element , readOwnHints : boolean ) : void {
@@ -76,7 +82,7 @@ function unmarkHolders(element: Element): void {
7682 let holder = element . parentElement ?. closest ( MARKED ) ;
7783 while ( holder ) {
7884 if ( escapes ( element , getComputedStyle ( holder ) . zIndex !== "auto" ) ) {
79- holder . removeAttribute ( PREVIEW_RASTER_ATTR ) ;
85+ setMark ( holder , false ) ;
8086 }
8187 holder = holder . parentElement ?. closest ( MARKED ) ;
8288 }
@@ -97,8 +103,8 @@ function readHintRules(): string {
97103 }
98104 } ;
99105 try {
100- for ( const sheet of [ ...document . styleSheets , ...( document . adoptedStyleSheets ?? [ ] ) ] ) {
101- collect ( sheet . cssRules , false ) ;
106+ for ( const rules of [ ...document . styleSheets , ...( document . adoptedStyleSheets ?? [ ] ) ] ) {
107+ if ( rules !== sheet ?. sheet ) collect ( rules . cssRules , false ) ;
102108 }
103109 document . createDocumentFragment ( ) . querySelector ( selectors . join ( "," ) || "*" ) ;
104110 } catch {
@@ -114,7 +120,9 @@ function hintRules(): string {
114120}
115121
116122function mayHint ( element : Element ) : boolean {
117- if ( ( element as HTMLElement ) . style ?. getPropertyValue ( "will-change" ) ) return true ;
123+ if ( marked . has ( element ) || ( element as HTMLElement ) . style ?. getPropertyValue ( "will-change" ) ) {
124+ return true ;
125+ }
118126 const rules = hintRules ( ) ;
119127 return rules !== "" && element . matches ( rules ) ;
120128}
@@ -128,14 +136,15 @@ function followTree(root: Element, batch: FollowBatch): void {
128136 decidedBeforeAnyWrite . push ( [ element , mayHint ( element ) && fits ( element , readOwnHints ) ] ) ;
129137 }
130138 for ( const [ element , drop ] of decidedBeforeAnyWrite ) writeMark ( element , drop ) ;
139+ if ( ! marked . size ) return ;
131140 for ( const [ element ] of decidedBeforeAnyWrite ) unmarkHolders ( element ) ;
132141}
133142
134143type FollowBatch = { changed : Set < Element > ; checked : Set < Element > } ;
135144
136145// Scripts restyle inline every frame, so only what can break a mark is looked at.
137146function followStyle ( element : Element ) : void {
138- if ( element . hasAttribute ( PREVIEW_RASTER_ATTR ) ) {
147+ if ( marked . has ( element ) ) {
139148 const style = getComputedStyle ( element ) ;
140149 if ( style . position === "static" || inlineFit ( element , style ) !== markedWith . get ( element ) ) {
141150 check ( element , true ) ;
@@ -145,9 +154,8 @@ function followStyle(element: Element): void {
145154}
146155
147156function clearMarks ( ) : void {
148- for ( const element of document . querySelectorAll ( MARKED ) ) {
149- element . removeAttribute ( PREVIEW_RASTER_ATTR ) ;
150- }
157+ for ( const element of document . querySelectorAll ( MARKED ) ) setMark ( element , false ) ;
158+ marked . clear ( ) ;
151159}
152160
153161// Every mark is decided before any is written, so the page restyles twice, not once per layer.
@@ -157,7 +165,8 @@ function markLayers(): void {
157165 const layers = [ ...document . querySelectorAll ( "*" ) ] . filter (
158166 ( element ) => mayHint ( element ) && fits ( element , true ) ,
159167 ) ;
160- for ( const layer of layers ) layer . setAttribute ( PREVIEW_RASTER_ATTR , "" ) ;
168+ for ( const layer of layers ) setMark ( layer , true ) ;
169+ watch ( ) ;
161170}
162171
163172function followAttribute ( record : MutationRecord , batch : FollowBatch ) : void {
@@ -186,17 +195,33 @@ function ownAttributeChanges(records: MutationRecord[]): Set<Element> {
186195 return changed ;
187196}
188197
198+ // With nothing marked and no rule giving a hint, no attribute or inline style change can make or
199+ // break a mark: only added elements are followed, and a hint missed that way stays as authored.
200+ function watch ( ) : void {
201+ const attributes = marked . size > 0 || hintRules ( ) !== "" ;
202+ if ( ! observer || attributes === watchingAttributes ) return ;
203+ watchingAttributes = attributes ;
204+ observer . observe ( document . documentElement , { subtree : true , childList : true , attributes } ) ;
205+ }
206+
207+ function forgetRemovedMarks ( records : MutationRecord [ ] ) : void {
208+ if ( ! records . some ( ( record ) => record . removedNodes . length ) ) return ;
209+ for ( const element of marked ) if ( ! element . isConnected ) marked . delete ( element ) ;
210+ }
211+
189212function follow ( records : MutationRecord [ ] ) : void {
190213 const rules = records . flatMap ( addedRules ) ;
191214 for ( const rule of rules ) {
192215 rule . addEventListener ( "load" , ( ) => observer && markLayers ( ) , { once : true } ) ;
193216 }
194217 if ( rules . length ) return markLayers ( ) ;
218+ forgetRemovedMarks ( records ) ;
195219 const batch : FollowBatch = { changed : ownAttributeChanges ( records ) , checked : new Set ( ) } ;
196220 for ( const record of records ) {
197221 if ( record . type === "attributes" ) followAttribute ( record , batch ) ;
198222 else for ( const node of record . addedNodes ) if ( isElementNode ( node ) ) followTree ( node , batch ) ;
199223 }
224+ watch ( ) ;
200225}
201226
202227function restore ( ) : void {
@@ -205,6 +230,7 @@ function restore(): void {
205230 sheet ?. remove ( ) ;
206231 sheet = null ;
207232 hintRuleSelector = null ;
233+ watchingAttributes = null ;
208234 clearMarks ( ) ;
209235}
210236
@@ -218,9 +244,5 @@ export function setPreviewRasterScale(scale: number): void {
218244 ( document . head ?? document . documentElement ) . append ( sheet ) ;
219245 markLayers ( ) ;
220246 observer = new MutationObserver ( follow ) ;
221- observer . observe ( document . documentElement , {
222- subtree : true ,
223- childList : true ,
224- attributes : true ,
225- } ) ;
247+ watch ( ) ;
226248}
0 commit comments