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

[11.x] Eager asset prefetching strategies for Vite #52462

Merged
merged 6 commits into from
Aug 16, 2024

Conversation

timacdonald
Copy link
Member

@timacdonald timacdonald commented Aug 13, 2024

This PR adds the ability for applications to eagerly prefetch JavaScript and CSS chunks generated by Vite. The goal is to reduce the network delay / costs when navigating throughout a SPA front-end.

Applications built with Vite will often use "code splitting". This technique splits the JavaScript (and CSS) into smaller "chunks". When you load any given page, only the chunks required to render that page are loaded which leads to faster load times for applications. For example, when you land on the homepage you do not pay the cost of also downloading and parsing the admin dashboard JavaScript.

Below is an example of the chunks generated by a fresh Laravel Breeze application. You will notice some of these are pages, e.g., Dashboard, while others are individual components, e.g., PrimaryButton.

Screenshot 2024-08-16 at 09 07 01

On subsequent navigation, the browser then needs to download the next page's chunks. This could be one or more files. For most situations, this will involve the browser making a request to download the assets and another request to download the data from the server.

Below is an example of the chunks downloaded when navigating from Breeze's dashboard page to its profile page.

Screenshot 2024-08-16 at 09 12 46

Some front-end frameworks have improved this situation by prefetching assets on link hover or when the link enters the viewport. The Tailwind documentation site prefetches assets when the link enters the viewport.

Screen.Recording.2024-08-16.at.09.16.58.mov

The on-hover and enter-viewport technique works well for static website links, but not so well for stateful redirects to dynamic locations or forms that result in redirects to other locations after a POST, PUT, or other stateful request. It also does not always work when a user clicks a link while the asset is currently downloading. In that case, the browser may trigger another high priority request to download the asset.

This PR introduces a new technique for Laravel where an application can prefetch all chunks in the background on initial load. This retains the initial load optimisation introduced with chunking and improves subsequent navigation as assets have already been downloaded.

There are two strategies:

  1. Waterfall with configurable concurrent download limit
  2. Aggressive

The waterfall technique allows you to specify how many assets should be concurrently downloaded. This allows you to manually control how much network is used by the prefetching strategy.

The below call to usePrefetchStrategy will use a default of 3 concurrent downloads.

// AppServiceProvider.php

use Illuminate\Support\Facades\Vite;

public function boot()
{
    Vite::usePrefetchStrategy('waterfall');
}

Here is a very throttled load of a fresh Breeze application to illustrate the waterfall prefetching. After page load, which is represented in the timeline by the pink vertical line, the assets start concurrently downloading with a maximum of 3 active at any time.

Screen.Recording.2024-08-16.at.09.42.27.mov

The number of concurrent downloads may be customised by passing ['concurrently' => $count].

// AppServiceProvider.php

use Illuminate\Support\Facades\Vite;

public function boot()
{
    Vite::usePrefetchStrategy('waterfall', ['concurrently' => 1]);
}
Screen.Recording.2024-08-16.at.10.04.33.mov

The aggressive strategy will ask the browser to download all assets at once. Prefetch links, including those generated by the waterfall strategy, will ask the browser to download the assets with a low priority.

// AppServiceProvider.php

use Illuminate\Support\Facades\Vite;

public function boot()
{
    Vite::usePrefetchStrategy('aggressive');
}
Screen.Recording.2024-08-16.at.10.05.51.mov

Vite adds a content hash to each chunk. When an application makes a change to a component only that chunk will have a new file name. If a browser has recently visited a site and then returns after a change to a single component, only the one component would need to be downloaded by the browser while prefetching. Although the requests for already downloaded assets will show up in the network tab they will show as cached.

Screen.Recording.2024-08-16.at.10.14.59.mov

Inertia-like front-end frameworks could benefit from these prefetching techniques even more. As they are back-end routed, it means that the response from the server determines which chunks are required to render the page. Due to this, the browser cannot start download the assets until the server response has been received. With these strategies in place, the server response is the only network traffic required to navigate between pages.

The prefetching will only load the JavaScript and CSS. It does not support prefetching images or other asset types.

Notes:

  • Safari does not currently support prefetching, although it is available behind a feature flag and coming soon.
  • Firefox and Opera desktop do not currently support fetch priority.

See:

Copy link

Thanks for submitting a PR!

Note that draft PR's are not reviewed. If you would like a review, please mark your pull request as ready for review in the GitHub user interface.

Pull requests that are abandoned in draft may be closed due to inactivity.

@SuperDJ
Copy link
Contributor

SuperDJ commented Aug 14, 2024

Just as a question but perhaps something to consider or as additional feature. Would this also allow "page" prefetching when using Inertia? inertiajs/inertia#223

@timacdonald
Copy link
Member Author

@SuperDJ, that is outside the scope of this PR.

@timacdonald timacdonald marked this pull request as ready for review August 16, 2024 00:33
@timacdonald timacdonald changed the title [11.x] Adds asset prefetching strategies [11.x] Eager asset prefetching strategies for Vite Aug 16, 2024
@taylorotwell
Copy link
Member

@SuperDJ stay tuned for that.

@taylorotwell
Copy link
Member

Added:

Vite::useWaterfallPrefetching(concurrency: 10);
Vite::useAggressivePrefetching();

@taylorotwell taylorotwell merged commit 9c481ac into laravel:11.x Aug 16, 2024
28 of 29 checks passed
@timacdonald timacdonald deleted the asset-prefetching branch August 20, 2024 23:21
@smortexa
Copy link
Contributor

@timacdonald Does it support nonce attribute?

@timacdonald
Copy link
Member Author

Good call, @smortexa.

Adding support #52558

@david-fairbanks42
Copy link

Awesome feature I'm employing immediately!
@timacdonald, my only thought is to add a configurable delay before the waterfall prefetch starts. Use case is if the application immediately loads API data it would help overall performance by reducing the number of concurrent request. Perhaps also adding a configurable interval delay for subsequent chunks.
I'd be happy to put in a PR if it sounds like an appropriate addition.

@BassemN
Copy link
Contributor

BassemN commented Aug 24, 2024

I can't find the documentation for the prefetching part at https://laravel.com/docs/11.x/vite, or I might be looking in the wrong place.

@timacdonald
Copy link
Member Author

@david-fairbanks42, I've opened a PR that allows fine-grained control of kicking off the prefetch process: #52574

I'm not sure about a delay between individual assets. I would rather set a lower concurrency, e.g., Vite::prefetch(concurrency: 1); all prefetch requests are set to fetchpriority=low.

@BassemN, docs are a work in progress.

@timacdonald
Copy link
Member Author

@BassemN, you can see the docs here: laravel/docs#9838

They reference a method that will be tagged in the next release (this week).

@david-fairbanks42
Copy link

@timacdonald, an event sounds like a great solution. Much more flexible. If my Monday doesn't bog me down to much, I'll check it out.

@david-fairbanks42
Copy link

@timacdonald, the event in #52574 does offer some wonderful flexibility, great solution.

The following is probably irrelevant since users of production code wont disable browser cache.

Doing some additional testing in Firefox and Chrome: The rel="prefetch" does pull the chunk file, but when the page related to the chunk is loaded, the file is downloaded again. Removing the file from the file system, then navigating to the page throws an error because the file doesn't exist for the actual module loading. Using rel="modulepreload" instead allows the file to be removed from the file system and the page still load. With browser caching turned on, the page will load without error since it loads the module from cache.

Firefox got the modulepreload in version 115 (current is 128) while Chrome has had it since version 66. Mozilla Docs

The use case I am trying to address specifically is application update when users are actively using the system. I already have signalling in place to prompt them to reload their browser window to get the latest code. What I'm trying to avoid is having to leave old versions of the the compiled JS code available and some strategy to clean it up after a reasonable period. Forcing immediate browser reload would not be a good user experience (mid filling out a form for instance).

@timacdonald
Copy link
Member Author

I'm not seeing that behaviour.

Assets are loaded from the cache, rather than re-downloaded, after the initial prefetch runs. They appear in the "Network" tab but they are not loaded from the server.

Screenshot 2024-08-28 at 10 04 23

You will need to ensure you have cache headers set up. If the files are not allowed to be cached they will always be re-downloaded.

Could you please record a video showing the assets, their headers, and anything else relevant if you are still having issues with assets being re-downloaded.

I don't believe modulepreload is what we want to use. We use that for the current page assets, however these are a lower priority fetch, so prefetch seems like the correct mechanism.

@david-fairbanks42
Copy link

@timacdonald, thanks for the response. You're correct about the caching behavior. That's why I thought it was irrelevant.

I look forward to event trigger being released!

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.

6 participants