framework
framework copied to clipboard
[9.x] Support preloading assets with Vite
Preloading is a mechanism that informs the browser which assets are required for the current page load. You can read more about preloading on web.dev.
Using this PR on a real-world application increased the Lighthouse score from 59
to 90
. These were the average results from a controlled environment. Your mileage may vary, but this PR should generally work for the 95% usecase (IMO).
Preloading is not something to opt into. With this PR it becomes the default behaviour for applications using Vite, however the middleware does need to be applied manually if you want to have the preloading headers sent with the initial Laravel HTTP response.
I'll outline the problem we are looking to solve: for SPA applications building with Vite, or any modern build tool using code splitting, the resulting assets look similar to the following:
-
app.123.js
-
Welcome.123.js
-
Login.123.js
-
Register.123.js
- ...
Each of these top-level "pages" will often have imports:
-
Login.123.js
-
TextInput.123.js
-
InputLabel.123.js
-
PrimaryButton.123.js
-
"Code splitting" like this is a performance optimisation so you load only the JavaScript required for the current page, which makes the initial page load faster.
Currently in Laravel, when loading the very first page of an application, we specify the following entry point:
@vite(['resources/js/app.js'])
This results in the following waterfall of requests:
-
/login.html
... -
app.js
... -
Login.js
,TextInput.js
,InputLabel.js
,PrimaryButton.js
...
This waterfall of requests is, of course, less than ideal. Working through this waterfall:
- The browser must first download the HTML and parse it to know what CSS and JavaScript the document has specified. Our document has specified "app.js", so the browser proceeds to fetch it.
- Once
app.js
js fetched and parsed, the JavaScript kicks in an informs the browser that it should be loading theLogin.js
file and it's imports. - The browser begins fetching, parsing, and executing the required imports.
Possible solutions are:
- Bundle everything into a single
app.js
with "eager globing" - Preloading
Before I dig into these solutions, let me post the results of my Lighthouse performance testing against a real-world application. These tests were run against "mobile".
I acknowledge that this is a single application and need further verification.
With Preloading | Configuration | Lighthouse score |
---|---|---|
YES | Code split + vendor split | 90 |
YES | Code split | 90 |
NO | Code split + vendor split | 85 |
NO | Code split | 85 |
YES | Eager glob + vendor split | 65 |
NO | Eager glob + vendor split | 63 |
YES | Eager glob | 61 |
NO | Eager glob | 59 |
We can see in all instances, preloading had a positive impact on the Lighthouse scores.
Lighthouse screenshots
Although all these screenshots are of the same page, I found similar results across other pages with the application.
Preloading w/ Code split + vendor split
Preloading w/ Code split
Not Preloading w/ Code split + vendor split
Not Preloading w/ Code split
Preloading w/ Eager glob + vendor split
Preloading w/ Eager
Not preloading w/ Eager glob + vendor split
Not preloading w/ Eager glob
Although eager globing does solve the waterfall issue, I believe that the Vite setup for Laravel should also support code splitting the best it can. Preloading is utilised by Vite itself when doing things "the Vite way". This is the mechanism that all code splitting usually works. Additionally, if you utilise vendor splitting with eager globing, you are back to the original waterfall problem, as the app.js
has to be fetched and parsed before vendor.js
is requested and parsed.
With this PR in place, we are able to, at best, remove the waterfall entirely for all configurations:
-
/login.html
,app.js
,Login.js
,TextInput.js
,InputLabel.js
,PrimaryButton.js
...
and at worst, the non-SPA waterfall:
-
/login.html
-
app.js
,Login.js
,TextInput.js
,InputLabel.js
,PrimaryButton.js
...
This PR works by adding "preload" <link
tags into the HTML for any asset required by the current page and if used the middleware will send preload Link:
headers to the browser.
Preload tags are generated for the JavaScript and CSS entry points and their imports.
To take full advantage of this, applications do need to specify the page / chunk they are currently trying to load in the Vite tag:
@vite(["resources/js/Pages/{$page['component']}.vue"])
Applications do not have to update the Vite directive may still find this PR beneficial, as it can send the headers with the document instructing the browser to start fetching the app.js
and any additional CSS it imports.
With that in mind, preloading is beneficial to all configurations: eager glob, vendor split, code split, etc.
Note Applications that are vendor splitting will still need to add the
app.js
into the Vite directive:
@vite([
'resources/js/app.js',
"resources/js/Pages/{$page['component']}.vue",
])
Browser support
For CSS files, we have great browser support.
For JavaScript "Module preloading" unfortunately out of the main browsers Firefox and Safari do not currently support this feature. According to caniuse 73% of browsers in use globally support this feature. Their stats are provided by statscounter.
A lot of JavaScript eco-system is relying on this feature and we are all waiting for Firefox and Safari to roll the feature out.
The good news is that this feature has no negative impacts on un-supprted browsers. They simply ignore the tag and move on with their existing behaviour.
However, because CSS preloading is supported, applications will still likely get a boost in Firefox and Safari.