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

[ENH] Browser Caching for Static Assets #5005

Open
mdmontesinos opened this issue Jan 20, 2025 · 14 comments
Open

[ENH] Browser Caching for Static Assets #5005

mdmontesinos opened this issue Jan 20, 2025 · 14 comments

Comments

@mdmontesinos
Copy link
Contributor

Oqtane Info

Version - 6.0.1
Render Mode - Static
Interactivity - Server
Database - SQL Server

Describe the enhancement

As discussed in #4755 (comment), Oqtane won't be able to take advantage of the new MapStaticAssets capability to improve performance for static assets loading.

Still, I believe Oqtane should try to include some of its functionalities, like setting caching headers so that browsers can cache them. Basically, adding "Cache-Control" header in the UseStaticFiles response, as stated in https://learn.microsoft.com/en-us/aspnet/core/fundamentals/static-files?view=aspnetcore-9.0#set-http-response-headers.

This would be the easiest of the techniques to implement, it wouldn't affect framework performance and it improves Lighthouse performance metrics. For example, this was taken from the Oqtane.org site:

Image

For busting the cache, in #4745 it was discussed that a framework level hashing/fingerprinting was not necessary, and developers should add querystrings with versioning for this purpose.

I assume this caching should only affect static assets and exclude the ones uploaded via the framework File Manager, but it's still an improvement.

Anything else?

@sbwalker
Copy link
Member

sbwalker commented Jan 22, 2025

@mdmontesinos It should be noted that this enhancement will impact a variety of scenarios and could be considered a breaking behavioral change (as the framework will behave differently than it has traditionally).

For local software development on modules/themes, it will result in static assets being cached which means developers will need to introduce versioning mechanisms into their code (which was never required previously) or they will need to hard-refresh their browser during development. For production environments, when upgrading the application/modules/themes there may be new versions of static assets which are included - however older versions will be cached, and as a result users may experience broken functionality after an upgrade (ie. they will need to hard-refresh their browser - which is a challenging scenario for many non-technical users). When uploading content files which replace existing files, if the direct path to the file is used (rather than the Files server) the old version will be cached and will be served rather than the new version.

Based on these breaking changes I am hesitant to include this enhancement as it has the potential to create a lot of support problems. Some of these challenges can be mitigated through configuration ie. disable caching in development environments. Other challenges would require developers to change the way they develop (e. the Oqtane framework itself as well as third party modules and themes would need to use versioned assets). In order to avoid unexpected behavior, perhaps this feature would need to be opt-in ie. an option set per site. However this still would not solve the broader "upgrade" problem - as it would require asset versioning.

@mdmontesinos
Copy link
Contributor Author

@sbwalker Thanks for pointing out additional considerations about the topic. You're right that it has some complications, but I really believe this change is vital to enhance the capabilities of Oqtane. Almost all modern frameworks include mechanisms for assets caching, which greatly improves performance for the initial load.

Now that we've confirmed that the "/files" server does not go through the static assets middleware, perhaps it would be possible to use the new MapStaticAssets capability to solve some of the issues presented. This would handle all the cache invalidation with fingerprinting so developers don't have to manually version assets and provide other improvements. Obviously, this also has some considerations:

  • The "Content" folder should be excluded as the assets there are not static and can't be determined in build time. And as it was mentioned in Fixes #5005: Add Browser Caching for Static Assets #5007, the "/files" server should be used instead and it already includes some caching mechanisms.
  • To avoid module/theme developers having to add versioning themselves, we should find a way to create the assets manifest individually for each module/theme during the build/publish process and then copy it to a specific folder (i.e. "manifests") in Oqtane when installing them.
  • As when modules/themes are installed a restart is required, the framework could iterate on startup over all the manifests and combine them into a single file which is what the MapStaticAssets method expects.
  • This would require little additional configuration from developers, perhaps just adding the build step to generate the manifest, and the rest would be handled by Oqtane.
  • Additionally, if a module/theme does not include its assets manifest, perhaps it could just be ignored and handle its resources in the regular way, without the enhancements as a fallback.
  • This mechanism would be clearly aligned with Microsoft's way of implementing it.

I believe this takes care of most of the problems you described previously, at the expense of only affecting truly static assets directly bundled from modules/themes and not from the dynamic file management system in Oqtane. Still, a big performance boost.

Also, it could also be made opt-in per site just in case.

Please let me know your opinion about this.

@sbwalker
Copy link
Member

sbwalker commented Jan 22, 2025

@mdmontesinos by default when you build a .NET 9 project in it will produce a static asset manifest in the /bin folder named:

{AssemblyName}.staticwebassets.endpoints.json

This file could easily be included by the debug.cmd and *.nuspec files in the Module/Theme Package project so that it is deployed in the same way as standard .NET assemblies

MapStaticAssets has the ability to point to multiple manifest files, so they do not need to be combined into a single file on startup - instead all individual manifest files could be registered during app startup.

I really wish Microsoft had implemented MapStaticAssets using a different approach. Rather than optimizing during build, it would have been possible to lazily optimize assets at run-time ie. when a static asset is first requested it would be dynamically optimized. This approach would have avoided the need for static manifests and would support static assets deployed in ANY manner to a server, including user uploaded files. It would also be much more efficient, as instead of loading a collection of every possible static asset into memory during startup, it would progressively build the collection in memory based on the static assets which are actually used in the application. MapStaticAssets is a good idea but it feels like it was not implemented for a real world scenario.

@mdmontesinos
Copy link
Contributor Author

@sbwalker Taking into account what you said, it should be technically possible to implement it in a "relatively" easy and non-disruptive way, right?

Although perhaps you are still not convinced that it's the correct approach from your last sentence.

MapStaticAssets is a good idea but it feels like it was not implemented for a real world scenario

@sbwalker
Copy link
Member

@mdmontesinos what I am struggling with is the dilemma between the Microsoft Alignment principle in the Oqtane Philosophy (https://www.oqtane.org/blog/!/20/oqtane-philosophy) and the fact that a lot of the capabilities provided by Microsoft are unfortunately designed for static application scenarios and therefore are not a great fit for Oqtane (due to its dynamic nature).

@sbwalker
Copy link
Member

sbwalker commented Jan 23, 2025

Some additional items to note...

Because MapStaticAssets relies on pre-optimization during the build stage, Nuget packages would need to include all of these optimized assets (ie. *.br, *.gzip) which will make the packages much larger.

There is also no easy way to manage which files are included or excluded in the staticwebassets.endpoints.json files. Basically everything in wwwroot is included - which could include a variety of supporting files which are never expected to be served as content.

And for those people who currently deploy granular assets manually using FTP, etc... it would no longer be possible without also deploying the optimized assets and the updated staticwebassets.endpoints.json.

@mdmontesinos
Copy link
Contributor Author

a lot of the capabilities provided by Microsoft are unfortunately designed for static application scenarios and therefore are not a great fit for Oqtane (due to its dynamic nature)

@sbwalker I totally understand your dilemma, but the alternatives are using 3rd party software (which is totally not aligned with Oqtane's philosophy), implementing a whole optimization/caching system for Oqtane from scratch to support dynamic assets, or perhaps settling in the Microsoft's approach as a starting point to cover at least the static assets. None of the options are ideal, but the Microsoft's approach requires the least amount of work (I hope).

As for the additional notes

Because MapStaticAssets relies on pre-optimization during the build stage, Nuget packages would need to include all of these optimized assets (ie. *.br, *.gzip) which will make the packages much larger.

Yes, that's true, but that will be the case for every nuget package in .NET 9 that includes assets, so Oqtane wouldn't be an exception. Also, this should be opt-out (or even opt-in) for developers in case they prefer not optimizing the assets. Oqtane should use the regular static assets middleware as a fallback.

There is also no easy way to manage which files are included or excluded in the staticwebassets.endpoints.json files. Basically everything in wwwroot is included - which could include a variety of supporting files which are never expected to be served as content.

IMHO, if they are never expected to be served as content, they shouldn't even be included in wwwroot in the first place, as it's publicly available. For example, I believe module and theme templates shouldn't be there, as custom templates might contain details about code infrastructure which can be used to gather information and discover vulnerabilities.

And for those people who currently deploy granular assets manually using FTP, etc... it would no longer be possible without also deploying the optimized assets and the updated staticwebassets.endpoints.json.

And as for this, again, the assets optimization should be optional for developers in case they don't want to deal with the drawbacks you mentioned (but also not obtain the performance enhancements).

@sbwalker
Copy link
Member

It turns out that my earlier comments related to "This file could easily be included by the debug.cmd and *.nuspec files in the Module/Theme Package project so that it is deployed in the same way as standard .NET assemblies" is not correct...

In .NET 9 the actual optimized static assets will only be created if you perform a Publish on a project (ie. you need to select the Server project in your solution, select Build... Publish... create a Publish Profile including a folder to publish to.... select Publish). This will create a wwwroot folder within the Publish folder which includes the optimized assets.

This is a fairly significant change for module/theme developers in terms of how to develop/deploy their artifacts to Oqtane. In addition, it creates challenges for the debug.cmd and *.nuspec files in terms of where to locate the static assets. This would likely require a new set of module/theme templates in order to use this approach.

@sbwalker
Copy link
Member

At this point it is clear that MapStaticAssets has MANY disadvantages... so I believe it makes sense to explore alternative approaches.

@mdmontesinos
Copy link
Contributor Author

That's really unfortunate.

Have you got any suggestions for alternatives?

@sbwalker
Copy link
Member

sbwalker commented Jan 23, 2025

The UseStaticFiles middleware has always supported eTags since .NET Core was first introduced. What it does not support is client caching or fingerprinting. #5007 provides a simple way to include client caching - but this has problems outlined earlier in this thread if assets change. Oqtane already supports fingerprinting by manually adding a version querystring to Resource Urls (note that in .NET 9 and MapStaticAssets you do not get fingerprinting for free either - you need to modify your code to use the new Assets[] concept). Oqtane could easily be enhanced to add the module or theme version number to the associated Resource Urls at run-time (ignoring Resource Urls which already have a querystring specified) which would provide an automatic fingerprinting mechanism. The core framework assets can use the Framework version. This approach would then support client caching and cache busting. Note that this is just an idea - it has not been tested.

@sbwalker
Copy link
Member

I have been able to validate the concept I described above - it is possible to fingerprint Resources using the version of the Theme or Module they belong to... which serves as an effective cache buster when upgrading components:

Image

@mdmontesinos
Copy link
Contributor Author

That's nice! Are there any use cases in which assets are updated but not the module version? For example, you mentioned manually deploying with FTP.

@sbwalker
Copy link
Member

Absolutely... if you deploy individual static assets (ie. using FTP, etc...) the old version will be still be served.... this is the big problem with client caching. A related problem is that the .NET Core middleware was not designed to support a multi-tenant environment - so you have to enable it for your entire installation (even if some sites in the installation would like to opt out of caching).

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

No branches or pull requests

2 participants