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

BUGFIX: Improve UI loading performance by removing UI nodedata script tag and using less data #3770

Merged
merged 13 commits into from
Aug 12, 2024

Conversation

Sebobo
Copy link
Member

@Sebobo Sebobo commented Apr 24, 2024

What I did

With this change the minimal required nodedata for each node in the rendered
content is inserted as data attribute and not as inline script anymore.
This improves performance as no extra function call is executed for each node.

Additonally the reduction in rendered node attributes reduces the output filesize and again improves loading time.

To prevent just-in-time loading of nodes all incomplete nodedata is requested after the guest frame has finished loading.

One big benefit of that is that the scripts tags won't interfere with JS plugins anymore and using the Fusion Augmenter will not wrap a single object with a ContentWrapping with another tag due to the script tag.

Fixes neos/neos-development-collection#2988

How to verify it

To test this change one can use the bugfix/remove-ui-script-tag-compiled-artifacts branch in composer:

"neos/neos-ui": "dev-bugfix/remove-ui-script-tag-compiled-artifacts as 8.3.8",
  • Check whether content is still properly editable
  • Inspector shouldn't show a loading spinner when content is selected
  • preview document should be smaller (10-30%) than before and load quicker (10-40%)
  • No script tags with node data in the raw preview html document
  • Load nodedata async after guest frame has loaded
  • Visualise for user that backend still loads data before page is fully editable

@Sebobo Sebobo added Bug Label to mark the change as bugfix 8.3 labels Apr 24, 2024
@Sebobo Sebobo self-assigned this Apr 24, 2024
@Sebobo
Copy link
Member Author

Sebobo commented Apr 24, 2024

I'm not sure how breaking this could be right now, therefore draft for now ;)

@grebaldi I remember you started working on a replacement for the script tag, so your insights here would be very welcome.

This doesn't need to be included in 8.3 but maybe we can :)

@Sebobo
Copy link
Member Author

Sebobo commented Apr 24, 2024

Some test results with 82 nodes (haven't found a good way to measure time to interactive with the iframe):

With Script Tag + with full Properties

Document size: 245KB / 14.6KB
Document rendering time: ~220ms
Transferred: 3.3MB
Finish: ~1.7s
Load: ~620ms
Requests: 28

Without Script Tag + with MinimalProperties + ondemand node loading

Document size: 198KB / 13.3KB (~80%)
Document rendering time: ~140ms (~65%)
Finish: ~1.6s
Load: ~620ms
Requests: 28

Without Script Tag + with MinimalProperties + Node batch loading

Document size: 198KB / 13.3KB (~80%)
Document rendering time: ~140ms (~65%)
Finish: ~2.1s
Load: ~660ms
Requests: 29

*
* @var Locale
*/
protected $rememberedUserLocale;
Copy link
Member

Choose a reason for hiding this comment

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

Are those changes related?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not a 100%, just some optimisation as the locale is switched twice for every node triggered by the augmentation

@Sebobo
Copy link
Member Author

Sebobo commented Apr 24, 2024

I tested a variant of this change in my customers project and the document size was reduced from 1.8mb to 1.2mb.
Without any nodedata it's 700kb. So I think we should invest further time in the future to really only include what we need.

Also there are two child nodes queries for each node to get document and content child nodes. Maybe we also don't need those for the initial rendering with some adjustments. Right now it would break some parts of the ui.

@Sebobo
Copy link
Member Author

Sebobo commented Apr 24, 2024

OK optimised a bit more and completely removed nodedata from the guest frame:

Without NodeData in augmenter

Document size: 108KB / 7.5KB (~44%)
Document rendering time: ~123ms (~55%)
Finish: ~2.1s
Load: ~610ms
Requests: 30

When clicking through the demo site each page is about 50% quicker to load which actually feels much nicer.

@bwaidelich
Copy link
Member

Great initiative, thanks for taking care.
Just one consideration: additional HTTP requests are fast locally, but they can be a bottleneck on remote servers. Not suggesting, that it is in this case but we should keep it in the back of our heads and test with throttled bandwidth

@Sebobo
Copy link
Member Author

Sebobo commented Apr 24, 2024

Yes of course. At the end we need the same amount of nodedata. But we kill the overhead of the function calls and document parsing.
Even if the combined loading time is the same as we load the nodedata, it feels faster as the page is already displayed much earlier while the data is fetched and the JS doesn't block the rendering. We send a bunch of queries at the initial load, so while the user starts moving the mouse to an element ideally the data is already there and simply be injected into the store instead of being parsed.
This makes especially a difference when you quickly navigate between pages.
The inspector also shows a loading spinner if the node is not fully loaded yet. So we have to check whether other places might need some visual feedback that we are still loading something if the connection is slow.

@Sebobo
Copy link
Member Author

Sebobo commented Apr 25, 2024

@bwaidelich I tested the loading performance a while yesterday with "Fast 3G" connection + Caching in my local Dev Context.

Result: The page loads much faster, but if you try to immediately edit text in a CKEditor, the first click only selects the element instead of showing the caret to start editing. The second click then works.

I want to test this on an actual remote server in Production Context to get a feeling for it.

I also checked whether I can adjust the LoadingBar to wait for the GuestFrame to be initialised, but this is not a simple change right now. I would make sense to have a loading indicator of some kind for the guest initialisation too. This is already currently problem, when initializeGuestFrame takes a bit longer. Just now with my change it takes a bit longer depending on the connection.
But the request runs in parallel with other requests like fetching node policies etc. So it's not simply adding to the loading time. But of course this depends a bit on the setup too.

We currently also load some nodes twice as the structure tree needs them too. I want to check whether I can then skip some of them.

@Benjamin-K
Copy link

Thanks for taking care of this. As a side effect, this will also help with some CSS selectors, as with the current approach selectors like .element:last-child don't work as expected in the backend.

@Sebobo
Copy link
Member Author

Sebobo commented May 6, 2024

Realised today this will also fix the issue neos/neos-development-collection#2988 with the Fusion Augmenter when augmenting a single content element. Previously it added another tag as it counted the element itself and the script tag instead of modifying the outside tag of the content element.

@Sebobo
Copy link
Member Author

Sebobo commented May 6, 2024

The two main performance hogs which makes the async request for the nodedata slow is in \Neos\Neos\Ui\Fusion\Helper\NodeInfoHelper::renderChildrenInformation which queries each nodes children twice.
In my tests this takes at least 50-60% of the duration.
The other one is the very inefficient conversion of each node context path from the query to an actual node.

It seems this was less of a problem in the previous version as during rendering the same first-level-node-cache was automatically used where possible.
I think this could be optimised quite a bit in a separate PR.

Right now I'm getting great results by optimising \Neos\Neos\Ui\ContentRepository\Service\NodeService::getNodeFromContextPath in skipping the creation of a completely new context with site and domain for each requested node.
For the child node query I don't have a great idea right now. This could be easier with Neos 9 maybe.

@Sebobo Sebobo force-pushed the bugfix/remove-ui-script-tag branch from 5242e1d to c439582 Compare May 7, 2024 06:34
Copy link
Contributor

@grebaldi grebaldi left a comment

Choose a reason for hiding this comment

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

@Sebobo

@grebaldi I remember you started working on a replacement for the script tag, so your insights here would be very welcome.

My idea was to replace the content element wrapping technique we're using right now (HTML Augmenter) with an alternative approach using HTML comments instead. This would prevent collisions with userland markup and I also expect it to perform better, since HTML augmentation is quite slow.

That idea does not at all interfere with your change here. I actually thought we had rid ourselves of those script tags already 🙈

Thanks a lot for taking care of it! 👍

I've tested it locally (unthrottled), and I did notice some lag with the guest frame initialization (especially noticeable with lots of greyed-out not-inline-editable elements). But I think the trade-off is well worth it and we can optimize the initialization later still.

I've also re-run the CircleCI e2e tests, which were red due to connectivity issues. Everything's green now :)

@Sebobo Sebobo marked this pull request as ready for review May 22, 2024 06:50
@Sebobo Sebobo force-pushed the bugfix/remove-ui-script-tag branch from c439582 to 701a047 Compare May 22, 2024 06:57
@Sebobo
Copy link
Member Author

Sebobo commented May 22, 2024

Hi @grebaldi thx for the review!

This morning while walking the 🦮 I thought about batch loading the nodes in the UI as a future optimisation. Meaning instead of loading all of them at once, We could load them in batches of 20(?) nodes or so and this way would make the nodes "above the fold" quicker accessible than the rest.
But we definitely also need to adjust the behaviour of the progress bar to actually show the state of the guest frame application instead of just the DomContentLoaded event. But would also do this in a separate PR

@Sebobo
Copy link
Member Author

Sebobo commented Jun 13, 2024

@bwaidelich what do you think, should we merge this change and open a follow up to rework the ui loading state for 8.4 f.e.?
Or combine both into 8.4 to be safe

@bwaidelich
Copy link
Member

what do you think, should we merge this change

I can't really give a qualified opinion (just wanted to chime in regarding potential pitfalls with n+1 requests) but my tendency would be to go the safer route (merge it into 8.4)

@mhsdesign
Copy link
Member

Fyi for up merging it has to be adjusted in Neos 9 in Neos.Neos https://github.com/neos/neos-development-collection/blob/bf4df1ea7445e482df9c2e6a9c647915f6a33a53/Neos.Neos/Classes/Service/ContentElementWrappingService.php#L108

@mhsdesign
Copy link
Member

As we dont profit anymore from the fusion cache the ajax request might actually be slower.
For example here

javascriptBackendInformation = Neos.Neos.Ui:RenderConfiguration {
path = 'documentNodeInformation'
context {
documentNode = ${documentNode}
site = ${site}
}
@position = 'after javascripts'
@process.json = ${Json.stringify(value)}
@process.wrapInJsObject = ${'<script>window[\'@Neos.Neos.Ui:DocumentInformation\']=' + value + '</script>'}
@if {
inBackend = ${documentNode.context.inBackend}
}
// We need to ensure the JS backend information is always up to date, especially
// when child nodes change. Otherwise errors like the following might happen:
// - create a new child (document) node
// - again visit the parent node
// - the parent node still has the stale "children" infos (without the newly created child)
// - thus, the document tree will have the newly created node REMOVED again (visually).
//
// as a fix, we ensure that the JS backend information creates an own cache entry, which is flushed
// whenever children are modified.
@cache {
mode = 'cached'
entryIdentifier {
jsBackendInfo = 'javascriptBackendInformation'
documentNode = ${documentNode}
inBackend = ${documentNode.context.inBackend}
}
entryTags {
1 = ${Neos.Caching.nodeTag(documentNode)}
2 = ${documentNode.context.inBackend ? Neos.Caching.descendantOfTag(documentNode) : null}
}
}
}
we explicitly cache the script tags hard for performance.

Copy link
Member

@mhsdesign mhsdesign left a comment

Choose a reason for hiding this comment

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

Had one more look at the code

Classes/Domain/Service/UserLocaleService.php Outdated Show resolved Hide resolved
packages/neos-ui-guest-frame/src/initializeGuestFrame.js Outdated Show resolved Hide resolved
packages/neos-ui-guest-frame/src/initializeGuestFrame.js Outdated Show resolved Hide resolved
mhsdesign added a commit that referenced this pull request Jul 15, 2024
@Sebobo
Copy link
Member Author

Sebobo commented Jul 19, 2024

@mhsdesign are we finished here now? I merge the current 8.3 and the node updates on click disappeared again thx to Wilhelms fix.

@Sebobo Sebobo force-pushed the bugfix/remove-ui-script-tag branch from 2d46ef2 to 2401318 Compare July 23, 2024 14:13
@Sebobo
Copy link
Member Author

Sebobo commented Jul 23, 2024

@grebaldi @mhsdesign one thing this PR changes is that rendering the page content will not update the stored nodes anymore with the state from the rendered nodedata.

It relies on the store which is mostly prefilled by the content tree before the content is rendered.
Afterwards switching between pages will not initiate new requests.

There it could be that changes of another user will be rendered in the content, but will not update the stored nodes.
Only refreshing the content tree will update those nodes, which is not intuitive. This could result in outdated information in the inspector if I'm not mistaken.

Option 1: We could let the content tree always load nodes from the server when switching pages.
Option 2: We introduce something like a timestamp or hash that tells us if our data is outdated for a page.

Wdyt?

@grebaldi
Copy link
Contributor

@Sebobo

I think Option 1 sounds quite fair :)

Option 2 may save some HTTP requests, but I don't think it's worth the effort.

To track content changes per document (via hash or timestamp), we would have to traverse each document's content tree anyway. In the best case we save an HTTP request, in the worst case, we do the traversal twice.

Sebobo and others added 13 commits August 8, 2024 09:34
…nodedata

With this change the minimal required nodedata for each node in the rendered
content is inserted as data attribute and not as inline script anymore.
This improves performance as no extra function call is executed for each node.

Additonally the reduction in rendered node attributes reduces the output filesize and again improves loading time.

To prevent just-in-time loading of nodes all incomplete nodedata is requested after the guest frame has finished loading.
…r guest frame

The content tree is loaded early and can load fully loaded nodes without
being slower as the response time is the same. But this way we can skip
lots of nodes from being loaded by the guest frame if they are already
present in the store.
…use the store

previously it was cumbersome to collect all the `nodes` that are really on the page. But it should make no difference to do the lookup on a bigger set.
…ry endpoint

that means `neosUiDefaultNodes` only needs one param.
Technically we use a `Set` most times when we invoke the `q` endpoint but seb convinced me that this filtering doesnt hurt and is probably more sane :D
This prevents showing outdated content, as the guest frame will not load new content by itself anymore.
@Sebobo Sebobo force-pushed the bugfix/remove-ui-script-tag branch from 2fd3193 to f4520af Compare August 8, 2024 07:41
@Sebobo
Copy link
Member Author

Sebobo commented Aug 8, 2024

@grebaldi @mhsdesign I removed some code in the content tree document change saga and this way the content tree always reloads the nodes and this way we have up-to-date content.
The UX is also still good, as the document node shows the spinner when switching between pages back and forth, but besides that, the content stays displayed and can be interacted with while the data is loading.

IMO we can merge then.

Copy link
Member

@mhsdesign mhsdesign left a comment

Choose a reason for hiding this comment

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

Thanks for all the effort

@mhsdesign mhsdesign merged commit ece6ece into 8.3 Aug 12, 2024
10 checks passed
@mhsdesign mhsdesign deleted the bugfix/remove-ui-script-tag branch August 12, 2024 09:05
mhsdesign added a commit to mhsdesign/neos-development-collection that referenced this pull request Aug 28, 2024
The `Classes/Aspects/AugmentationAspect.php` was adjusted via

neos/neos-ui#3770

but in Neos 9.0 it resides here in Neos.Neos

Thus we have to redo the adjustments.

Fixes partially neos#4951 by removing illegal php dependencies
mhsdesign added a commit that referenced this pull request Aug 28, 2024
Remove deprecated interpretation of inline scripts (`data-neos-nodedata`)

#3770
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
8.3 Bug Label to mark the change as bugfix
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants