Add defer-attributes to all javascripts if possible. Deferring javascript-loading and execution makes the browser start HTML-parsing, rendering and painting.
So some static interim content may be shown pretty early in (or even before) the discourse booting process. This should do for a faster user-perceived page loading speed on first page load.
"Before this change, an element being removed caused it to no longer be considered a valid LCP candidate. […] After this change, an element being removed is still considered a valid LCP candidate. "
“The change to include content which is later removed from the DOM as possible largest contentful paints will improve Largest Contentful Paint times on sites which have images [for Discourse:text elements] of the same size inserted multiple times. This is a common pattern for carousels, as well as some JavaScript frameworks which do server-side rendering.”
0s-2s - blank screen:
WebPageTest ignores defer for JavaScript and downloads all the JavaScripts before doing a first paint - this works correctly on a real device.
2.5s - LCP: static content from server-rendering
3.5s - Visual Change: logo loaded
6.5s - Visual Change: content from EmberJs rendering
Note on First Input Delay: Here I clicked on background immediately when the static content was first visible. Add my reaction time on top of this FID.
Extra note on percentiles below the bars in these graphs:
The percentiles are not that relevant as they only compare the meassured values with the origin values. The origin is a TYPO3 webpage with a subfolder install of Discourse.
I totally agree Discourse is very slow JS Heavy web-app, if we can defer CSS/JS FILES, it will hugely help speed up LCP, FCP, FID,CLS.
Would really help to see this getting live, we and many other people are facing this issue. All discourse sites are failing at Core Web Vitals. If we server a fast STATIC HTML page to users 1st time and or defer all JS/CSS logic in 1st initial load, and this way we can speed up all pages and pass CWV scores! excited to see this live on core discourse update.
All discourse sites Google rankings are declining due to sites not passing Core Web Vitals.
We’re open to experimenting with this in core. The ‘flash’ of differently-styled content can be a little disconcerting, so we’d like to start with it behind a default-disabled “experimental” site setting to start with. That way, Site admins can choose to enable it if they want to.
Are you able to try adding a site setting in your PR @rrit? It would also be good to add some RSpec tests to verify the behaviour with the setting enabled / disabled.
Doesn’t that means that we can simply put a fullscreen spinner (that has 100% width and 100% height) on the page rendered by the server, and replace it by the Ember app when it finally boots to get an extremely low LCP?
We could make this spinner a SVG that mimics the Discourse UI so the transition is smoother and less FOUC-like.
I had to disable defer tag for QUnit Test Runner: app/views/qunit/index.html.erb
Before QUnit Tests did still run with the feature flag "javascript defer" = false. And now the tests run also with "javascript defer" = true.
For text elements, only the size of their text nodes is considered (the smallest rectangle that encompasses all text nodes).
For all elements, any margin, padding, or border applied via CSS is not considered.
That’s why the static text-node must be rendered exactly the same size as the EmberJs text-node.
Or even slightly bigger by increasing the line-height.
E.g. if the width of the text-nodes doesn’t match, there are a lot of geometric cases introduced by different linebreaks where the static text-node becomes smaller than the EmberJs-one.
In the POC there are two features combined - shall we split them into two experimental feature flags?
JavaScript with defer-tag (feature flag in settings dashbord) (hidden feature flag as a container rebuild or theme cache flush is needed for this) ← Fix: hot-switching with cache
Showing static content in topic view (feature flag in settings dashbord)
Of course the full impact on LCP is only accomplished by using both: FCP: static content
There might be Discourse instances where plugins or theme components fail on the JS defering. By splitting these feature, they can have a small gain on the static content without defering JS: FCP: static content without JS defer
Let’s wait another 7-14 days to hopefully see more improvements for mobile pages as the values are averaged over the last 28 days - only 12 days counting with the POC applied right now.
@rrit thanks for sharing the data from your site! We’ve been discussing this internally, and I’m afraid we won’t be adding this functionality to Discourse core at the moment.
While the Web Vital metrics you shared are very impressive, the flash of ‘crawler view’ content doesn’t make for a great user experience. The styling changes you’ve made certainly help, but they will need to be tweaked for every Discourse site that has custom styling.
Our long-term aim is to implement true server-side-rendering using something like Ember FastBoot. Theoretically, that would provide the same statistical improvements that you’ve measured, while also providing a seamless user experience. We would prefer to focus our efforts towards that goal.
All that said, Discourse is super extensible, so I think it should be totally possible to implement your idea in a Discourse plugin and then share it here in #plugin.
The biggest change you’ve made in the core PR is to add the defer attribute to script tags. Overriding all those places from a plugin would be very difficult. However, I think the same result could be achieved with a middleware-based approach. I found this blog post which describes a similar problem:
Using that technique, you could write a middleware which checks for text/html responses, parses them, and then adds defer attributes where necessary.
Adding middleware from a plugin can be done something like this:
# name: my-plugin
# about: My plugin description
# version: 1.0
# url: https://example.org
require_relative "lib/script_defer_middleware"
on(:after_initializers) do
Rails.configuration.middleware.use(ScriptDeferMiddleware)
end
If you run into any roadblocks with a plugin-based approach, feel free to post here and we’ll be happy to try and point you in the right direction.