Defer javascript and show interim content on initial page load

How about: Deferring Discourse’ javascripts

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.

Ideas for static interim content:

  • splash screen with logo and loading spinner
  • topic view with posts from backend

POC and PR

For latest proof of concept and PR - please look into this post.


The vendor-javascript and all preceding javascripts are not deferred right now.
@see: Disable defer for vendor-script and all preceding scripts to keep ord… · rr-it/discourse@328efd5 · GitHub

Ideas on how to solve this are very welcome.


JavaScript async vs. defer vs. none

More about javascript loading options - including defer: Efficiently load JavaScript with defer and async
(This is not about speeding up the real discourse boot.)


Fastboot/rehydration

I read this article:
The conclusion over there appears to be an implementation of Fastboot/rehydration.
Is there a timeline for this?

4 Likes

That would cause the LCP to be still after the EmberJS boots and re-renders, not addressing the main problem regarding new Google rankings.

That’s our current midterm plan to address LCP in Discourse.

2 Likes

As of Chrome 88 this is luckily not true anymore! :rocket:
Didn’t know about this either until now. :))

https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/speed/metrics_changelog/2020_11_lcp_2.md

"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.”


LCP Changelog

There might be more good changes in the future:
https://chromium.googlesource.com/chromium/src/+/master/docs/speed/metrics_changelog/lcp.md

1 Like

Here are some simulated statistics for a topic page with the POC implemented.

Lighthouse: “Values are estimated and may vary.”

WebPageTest

Moto4G simulation


Note: we are the black arrow on top.


Notes:

  • 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
  • 7s - Visual complete

PageSpeed Insights

https://pagespeed.web.dev

Largest Contentful Paint element

PageSpeed correctly identifies the static text-node from server-rendering as FCP LCP element:
div.row > div.topic-body > div.post > p

EmberJs rendered text-node:
div.row > div.topic-body > div.regular.contents > div.cooked > p

But it looks like PageSpeed does not use the correctly identified static text-node for its simulated result: simulated FCP and LCP are to huge.

Real-world user-data

Let’s wait another 14-28 days to get “real” data from Chrome UX Report with the POC implemented.

Statistics without POC implemented for the tested topic page:
(The data is of this single topic url – and not of the entire origin.)

4 Likes

Oh that’s a very interesting find! Great work!

What does the you get on this extension https://chrome.google.com/webstore/detail/web-vitals/ahfhijdlegdabablpippeagghigmibma?

3 Likes

Via Web Vitals Chrome Extension

  • on desktop
  • Chromium Version 90.0.4430.212
  • first load on new incognito window


Note on First Input Delay: I waited until fully loaded page and then clicked on background - so after EmberJs rendering finished.


Note on First Input Delay: Here I clicked on background immediately when the static content was first visible. Add my reaction time :sloth: 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.

2 Likes

Awesome idea! @rrit

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.

2 Likes

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.

5 Likes

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.

2 Likes

I think the key part is LCP “candidate

It will only be considered the largest contentful paint if it really is the largest (or at least, the same size) as what is eventually rendered?

So using the crawler view works quite well because the content (i.e. the text) is largely the same?

(I am mostly guessing here, based on the screenshots above - I haven’t tried a fullscreen spinner)

1 Like

Feature flag is implemented.

I’m not at all a ruby developer - on this I definitely need some help.

Maybe push my POC into a new branch in the discourse/discourse repo, before doing a PR on main?

This is my PR on this feature:

@david Can you lend me your head for some help on developing Rspec tests for these changes:

app/helpers/application_helper.rb: spec/helpers/application_helper_spec.rb

I don’t see feasible unit tests here. It looks testable by integration tests only.
app/models/theme.rb
app/models/theme_field.rb

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.

2 Likes

This is probably already blocked by https://chromium.googlesource.com/chromium/src/+/master/docs/speed/metrics_changelog/2020_11_lcp.md:

Full viewport images, which are visually equivalent to background images, are no longer considered as the largest contentful paint


Good point: see Largest Contentful Paint (LCP)  |  Articles  |  web.dev

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.

See: LCP examples


I actually used the noscript-rendering of the posts inside a topic page. The CSS-classes do slightly match the real ones - so the look is equal.

See: Changes to app/views/layouts/application.html.erb

Edit: My fault, this is actually the crawler view: app/views/topics/show.html.erb

2 Likes

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)

Here we go: feature flags


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

2 Likes

First impression from Google Search Console with the POC applied since 2022-01-30:

Desktop

Desktop took some time for results to come in:
grafik
Note: the old green baseline represents non-Discourse webpages on the same domain.

Mobile

grafik
Note: the old green baseline represents non-Discourse webpages on the same domain.

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.

5 Likes

LCP summary on Proof of Concept

The POC is applied since 2022-01-30 and it took +4 weeks to affect all pages in Google Search Console “Core Web Vitals” report - based on CrUX data.

All topic pages are in the LCP green zone (measured by CrUX):

  • Desktop: LCP 1.7 sec
  • Mobile: LCP 2.0 sec

LCP data: Google Search Console/CrUX

Impression from Google Search Console with the POC applied since 2022-01-30:

Desktop


Note: the old green baseline represents non-Discourse webpages on the same domain.

Good URLs

grafik

Mobile


Note: the old green baseline represents non-Discourse webpages on the same domain.

Good URLs

grafik

LCP issue: longer than 2.5s (mobile)

grafik
Note: Only topic pages show static content before EmberJS-content


Approval of PR with feature flags needed

@sam May you delegate this PR to someone to take a look for approval, please.

3 Likes

We will certainly carefully review it, it is a very big change may take us a bit to get to it.

5 Likes

@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:

https://mensfeld.pl/2014/12/rackrails-middleware-that-will-ensure-relnofollow-for-all-your-links/

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.

10 Likes

If I find time for this, I will probably implement a plugin.

But for now I try to get along with a patching approach in web_only.yml:

# not tested pseudo-code!
hooks:
  after_code:
    - exec:
        cd: $home
        cmd:
          - curl https://patch-diff.githubusercontent.com/raw/discourse/discourse/pull/15858.diff | git apply
4 Likes

Ember FastBoot looks like a perfect long-term approach. Meanwhile the LCP topic stays hot:

2 Likes

Thank you for working on this @rrit :+1:

I have good news, we’ve implemented a new feature in Discourse, which should help with this quite a bit

5 Likes