Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: inserted styles lost when moving elements #1357

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

jaj1014
Copy link

@jaj1014 jaj1014 commented Nov 20, 2023

hello 👋 - I created an issue a couple weeks back and had some time to dig in a bit more to see if I could figure out a potential solution.

The issue linked above has more detail, but the tl;dr; is that styles which are inserted using CSSStyleSheet.insertRule() are lost during diff operations that result in that style element getting moved.

In order to better demonstrate the problem, I was finally able to create a recording (linked below): If you skip from 0:04 seconds to 0:15, you'll see the teal div at the top loses it's teal color. If you let it play through from 0:04 seconds to 0:15, you'll notice the teal div is still teal. This is because skipping triggers the style element which had the teal styles inserted gets moved as a result of the diff, so the inserted styles get lost. Letting it play through, they aren't lost because diff isn't run and things get mutated normally (w/o virtual dom).

https://rrwebdebug.com/play/index.html?url=https%3A%2F%2Fgist.github.com%2Fjaj1014%2F6e6658d0f24ea297d31edbcfdd44379e&version=2.0.0-alpha.8&virtual-dom=on&play=on

Because the issue has several things contributing to the issue, I wasn't sure where it made the most sense to apply a fix. I took a stab at applying a fix during the diff process inside buildFromNode to copy CSSRules from the dom node into the rrNode for style elements. Later in the diff flow, there is code that will take those style rules copied from the original and apply them.

Copy link

changeset-bot bot commented Nov 20, 2023

⚠️ No Changeset found

Latest commit: 74f887e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@jaj1014 jaj1014 force-pushed the ajax-copy-css-rules-from-style branch 3 times, most recently from d455e0b to 9c4d539 Compare November 27, 2023 15:46
@jaj1014
Copy link
Author

jaj1014 commented Nov 27, 2023

The original approach I took w/ copying cssRules into the virtual nodes created an issue w/ duplicate styles being applied after the diff. This was because that approach wasn't surgical enough and applied to all style sheets - not just the ones that were being moved. To address that, I moved the fix into the diff work to only copy inserted styles over when insertBefore() is being used.

I also added a test case in replayer.test that fails if you run w/o my changes (demonstrating the issue) and passes w/ my changes.

I'm not sure if the written code satisfies rrweb style/standards, so I haven't written tests for the 'util' functions that were added to diff.ts. Happy to adjust where that code lives based on comments. :)

@@ -718,7 +735,7 @@ describe('replayer', function () {
const replayer = new Replayer(events);
replayer.play();
`);
await page.waitForTimeout(50);
await page.waitForTimeout(150);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a more reliable way of forcing a wait

Suggested change
await page.waitForTimeout(150);
await waitForRAF(page);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated and thank you!

@jaj1014 jaj1014 force-pushed the ajax-copy-css-rules-from-style branch from 9c4d539 to c6de322 Compare December 1, 2023 12:35
@Juice10
Copy link
Contributor

Juice10 commented Dec 1, 2023

@YunFeng0817 could you check this one out, see if this is the best way to do this in rrdom? @eoghanmurray and I started reviewing this, tests look good, but we're also not sure if this is the best way to implement this.

@jaj1014 Thanks for taking the time to debug this and create a fix for it!

@YunFeng0817 YunFeng0817 self-requested a review December 4, 2023 10:43
@jaj1014 jaj1014 force-pushed the ajax-copy-css-rules-from-style branch 2 times, most recently from 404c950 to f4537e6 Compare April 19, 2024 17:46
@jaj1014
Copy link
Author

jaj1014 commented Apr 19, 2024

Finally got around to adding unit test coverage for the added functions.

@jaj1014 jaj1014 force-pushed the ajax-copy-css-rules-from-style branch 2 times, most recently from fb5143a to 8b10795 Compare April 22, 2024 19:19
if (
iframeSourceWindow == window.parent &&
window != window.parent &&
message.data.snapshot
Copy link
Contributor

@eoghanmurray eoghanmurray May 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annoyingly, typescript (eslint) is failing here; you could define your own message type (see CrossOriginIframeMessageEventContent) and cast to that message type with the as keyword, or maybe the following will work

Suggested change
message.data.snapshot
'snapshot' in message.data

@@ -93,7 +91,7 @@ export function initMutationObserver(
// see mutation.ts for details
mutationBuffer.init(options);
let mutationObserverCtor =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No idea what all the angularZoneSymbol stuff is, but now that this is no longer reassigned by that code, eslint requires us to change to a const

Suggested change
let mutationObserverCtor =
const mutationObserverCtor =

@jaj1014
Copy link
Author

jaj1014 commented May 8, 2024

sorry @eoghanmurray my branch got screwed up w/ some other commits. I'll try to get it clean up and back to only the changes pertinent to this PR. or worst case, close and open a clean one. sorry about this.

@eoghanmurray
Copy link
Contributor

eoghanmurray commented May 9, 2024

diff with actual and get only inserted styles

The new getInsertedStylesFromElement looks pretty cool; basically you are comparing raw text node rules to derived css rules and figuring out the diffs? This approach could potentially be useful during snapshotting as we've opted to go directly to .sheet.rules in case there were previous programmatically inserted rules (which doesn't always work; see #1322). I've got some in-the-same-space work in progress in #1437 findCssTextSplits()

For this PR though, I feel like it might be overkill and that it adds too much complexity; I feel like the underlying problem should be fixed instead (that elements don't get moved around after diffChildren). But maybe I'm missing something and this PR is the only way of doing it?

@jaj1014
Copy link
Author

jaj1014 commented May 9, 2024

cae6981 has the changes pertinent to the originally described problem. all the other commits got mixed in unintentionally. unsure if the complexity you're referring to is contained in this one commit, or if you're referring to the others.

you're correct on the gist of the code: it rectifies the issue caused by insertBefore dropping styles that were added with CSSStyleSheet.insertRule(). It does this by creating a copy of the stylesheet without those inserted rules and diffing the copy against the actual (before being moved) to get a map of the inserted styles it can apply after the element is moved.

It's been a while since I've been in this code, and what I thought I know may not even be right. I was thinking that moving elements was just applying the result of diffing with the virtualDom. When not using the virtualDom optimization, this issue doesn't happen. I'm don't know if it is possible to not move elements after diffChildren

@eoghanmurray
Copy link
Contributor

Ah okay; if you wish you could cherry-pick that commit onto an up to date trunk and push here.

Until then I'll leave it to @YunFeng0817 to look at the code and see if this is the best approach or if there is a simpler way of achieving the same thing with rrdom.

jaj1014 and others added 3 commits October 22, 2024 05:13
fix code for nodejs tests
change fix direction to avoid issues with duplicate styles
format issues
swap waitForTimeout for waitForRAF in test that flaked
Add unit tests for new functions
Fix broken test causes by file formatting removing spaced
@jaj1014 jaj1014 force-pushed the ajax-copy-css-rules-from-style branch from f890bc9 to cf4b340 Compare October 22, 2024 09:34
@billyvg
Copy link
Contributor

billyvg commented Jan 6, 2025

I've been trying this out and I think this causes a perf regression. I'm seeing the differ go from ~300-400 to ~600-700ms. The profiler indicates that getInsertedStylesFromElement is forcing reflow quite often.

@jaj1014
Copy link
Author

jaj1014 commented Jan 9, 2025

I've been trying this out and I think this causes a perf regression. I'm seeing the differ go from ~300-400 to ~600-700ms. The profiler indicates that getInsertedStylesFromElement is forcing reflow quite often.

Interesting. It makes sense that it's going to cause a performance hit to diff since it's running code that wasn't there before, but it should only run for style elements since those are the ones it's concerned with - and I'm not sure what in that function would cause a reflow...

Was there something specific you saw w/ getInsertedStylesFromElement that was causing the reflow?

The forEach that goes through the inserted styles found by getInsertedStylesFromElement that inserts onto the sheet... I could see each of those possibly triggering a reflow 🤔. But I'm also not super knowledgeable about profiler and didn't do testing for performance when I originally created this PR over a year ago.

@jaj1014
Copy link
Author

jaj1014 commented Jan 9, 2025

couldn't resist and was curious...

maybe because it uses innerText (sources: what forces layout/reflow from Paul Irish and an article that outlines differences)

Based on the article, it looks like .textContent instead would avoid the reflow. So line 627 would be: tempStyleSheet.replaceSync(styleElement.textConent);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants