dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.38k stars 10k forks source link

Include hash of Blazor WASM assemblies in URLs #17036

Closed mkArtakMSFT closed 4 years ago

SteveSandersonMS commented 4 years ago

I don't think there's anything else for us to do here. We now have the content hashes in the blazor.boot.json file (and service worker asset manifest for PWAs), and the .js code already takes care of requesting them with cache: no-cache when they are not already cached locally.

@javiercn Please comment if you think something else is still needed here, but I'll close now because I think this is now handled.

javiercn commented 4 years ago

Nope, looks good

MarkusRodler commented 4 years ago

@SteveSandersonMS But it would be nice that you don't have to download all assets on pwa update if you only changed one css file for example. I think the best way to save the most of the bandwith would be to generate hashed Urls (I mean the hash inside the path of the url or in the query string) and serving the "immutable" files with a long Cache-Control: max-age. With this approach only the modified files will be downloaded on service-worker update instead of all files. Jake Archibald has a good article to that topic: https://jakearchibald.com/2016/caching-best-practices/

I tried an alternative approach with completely ignoring the browser http cache but that does not work. I thought I could get the non-modified files from the previous cache(s) in the service-worker and only download the modified ones.

async function onInstall(event) {
// ...
    await caches.open(cacheName).then(cache => {
        cache.add(new Request('index.html'));
        let requestsToFetch = [];
        Promise.all(assetsRequests.map(async (request) => {
            const response = await caches.match(request);
            if (response) {
                return cache.put(request, response);
            }
            else {
                requestsToFetch.push(request);
                return Promise.resolve();
            }
        })).then(() => {
            return cache.addAll(requestsToFetch);
        });
    });
}
// await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));

But unfortunately this does not take the subresource integrity into account. So it will cache everything.

hultqvist commented 4 years ago

Will the current solution work when hosting the app on a CDN?

How does the blazor boot handle the case where the CDN still delivers the old content for some of the paths? If the service worker does hash validation it can only note that the files are wrong, it can't recover until all files are updated in the cache which could be 15 minutes.

If a third-party tool would generate hashes for the filenames, would it be enough to rename the files and update the paths in index.html, blazor.webassembly.js and blazor.boot.json?

Upon testing I found that I can update index.html and change filename of blazor.webassembly.js and blazor.boot.json. But once I try to rename files referenced in blazor.boot.json the runtime fail with an error.

However I was successful in adding a hash to the _framework folder and update paths in index.html and blazor.webassembly.js. Will this be a stable solution?

SteveSandersonMS commented 4 years ago

But it would be nice that you don't have to download all assets on pwa update if you only changed one css file for example.

As long as your server returns etags for the responses, the client won't re-download anything it already has. The server will return 304 Not Modified for those resources. No need for changing the URLs.

Will the current solution work when hosting the app on a CDN?

A CDN generally wants to host multiple versions of libraries so people can pick whatever version they need. So I'd expect a CDN to expose URLs like https://some.example.cdn/blazorwebassembly/3.2.0/dotnet.wasm (etc).

Note that the recently-added CDN support lets you generate any URLs you want. So for example you could programmatically add the first 8 chars of the integrity hash to the URL and request things like https://some.example.cdn/blazorwebassembly/your.application-4a30bcf2.dll.

MarkusRodler commented 4 years ago

But it would be nice that you don't have to download all assets on pwa update if you only changed one css file for example.

As long as your server returns etags for the responses, the client won't re-download anything it already has. The server will return 304 Not Modified for those resources. No need for changing the URLs.

@SteveSandersonMS The point is, that if I use "Cache-Control" on my server with a long max-age then my server will not be asked again if it were changed within that period of time (great 👍, less requests) but it will fail to update the service-worker because it will use the cached one and says nope, this file does not have a valid subresource integrity and stops the update process of the service worker.

I hope I was able to bring over my concern.

SteveSandersonMS commented 4 years ago

The cost of the requests that return 304 shouldn't be significant, because we don't request the _framework files at all once they are already cached.

We only request one of the _framework files per app startup, i.e., the blazor.boot.json file which tells us the hashes of all the other files, and hence we know whether to use the locally-cached copies based on those hashes.

As such there's literally no reason to use cache-control with a long max-age, since we're caching them permanently (that is, until their hashes in blazor.boot.json change) anyway.