Async css/js creation from #1014086 means there is no efficient way to get CSS from a library

Created on 3 December 2022, about 2 years ago
Updated 8 March 2024, 11 months ago

Problem/Motivation

The new async css/js created added in ๐Ÿ› Stampedes and cold cache performance issues with css/js aggregation Fixed depends upon generating the content in a separate request while previously the initial request generated the file and saved it to disk.

In the case of the s3fs module (streamWrapper provider) this set of changes will cause pages to no longer properly load. Links are generated to where the file should reside (on the remote server) however the file will now no longer exist which will cause a 404 for the assets resulting in pages lacking css styles and js code.

๐Ÿ› Breaking changes for public:// takeover in D10.1 from #1014086 Closed: outdated goes more in-depth about the changes that appear to be necessary in the s3fs module to work with D10.1

While I believe Async link generation is a good idea for long term ttfb performance, it appears to me this technically should not be enabled by default as it is currently designed until D11 to maintain BC.

๐Ÿ“Œ Document which parts of the module are still relevant after aggregation changes in 10.1.0 Needs review appears to indicate the changes may be breaking for non streamWrappers as well.

Steps to reproduce

Install D10.1-dev with s3fs configured for public:// takeover.
Visit a page and the links to the css/js will refer to the bucket (not the Drupal server), the css/js will return 404's and as such the page will not render correctly.

Proposed resolution

Make [# ๐Ÿ› Stampedes and cold cache performance issues with css/js aggregation Fixed opt-in until D11 to allow sites to choose when to enable it to prevent a BC break with contrib and custom code in a minor release.
Possibly provide deprecation notification (Likely in CssCollectionRenderer and JsCollectionRenderer) to to indicate a site needs to update.

Followups:
Investigate if CssCollectionRenderer and JsCollectionRenderer can be modified to obtain bubbleable metadata on css/js url generation and apply it to the render array to allow purging of links to generation controller after page building.

Remaining tasks

TBD

User interface changes

TBD

API changes

TBD

Data model changes

TBD

Release notes snippet

TBD

๐Ÿ› Bug report
Status

Postponed: needs info

Version

11.0 ๐Ÿ”ฅ

Component
Asset libraryย  โ†’

Last updated 4 days ago

No maintainer
Created by

๐Ÿ‡บ๐Ÿ‡ธUnited States cmlara

Live updates comments and jobs are added and updated live.
  • Contributed project blocker

    It denotes an issue that prevents porting of a contributed project to the stable version of Drupal due to missing APIs, regressions, and so on.

  • Regression

    It restores functionality that was present in earlier versions.

Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States cmlara

    With ๐Ÿ“Œ Make css/js optimized assets path configurable Fixed committed the issues should be solved for s3fs.

    Is there any need to keep this open for other modules as a meta or should we just close this out?

  • Status changed to Closed: outdated almost 2 years ago
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom catch

    There is ๐Ÿ“Œ Document which parts of the module are still relevant after aggregation changes in 10.1.0 Needs review and ๐Ÿ“Œ Support optimized assets path configurable Needs review open for advagg which is the other module likely to be most affected. I think we can close this.

  • Status changed to Active over 1 year ago
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom adamps

    This affects Drupal Symfony Mailer (now used on 11k sites), causing major bug ๐Ÿ“Œ Drupal 10.1.0 new aggregation breaks InlineCssEmailAdjuster Needs review , apparently making the module unusable on D10.1. Probably swiftmailer module is the same as it has similar code.

    The key code is this (in InlineCssEmailAdjuster):

        foreach ($this->assetResolver->getCssAssets($assets, TRUE) as $file) {
          $css .= file_get_contents($file['data']);
        }
    

    With ๐Ÿ› Stampedes and cold cache performance issues with css/js aggregation Fixed , the files don't exist yet and so file_get_contents() fails.

    The issue has a patch, but there are some problems:

    1. It requires code that checks \Drupal::VERSION, which we shouldn't have to do given core BC policy.
    2. It likely makes performance significantly worse, because the patched code entirely ignores the disk-based aggregate, and regenerates it for each email.

    Maybe what we need is an extra parameter on getCssAssets() that can be used to force generation of the assets? Older versions of Drupal would ignore the extra parameter.

  • ๐Ÿ‡ท๐Ÿ‡บRussia niklan Russia, Perm

    I want to add my few cents to this. The current CSS/JS aggregation is using similar approach to Image Style functionality. Which means, you can generate a result URI, but the actual file might not exist. Not always images, processed by Image Style, are requested by an HTTP request. For example, when generating some kind of file (PDF, XLSX etc.) that should include an image processed by Drupal, the libraries usually expect a relative path to the image, when they simply check for file_exists() and embed if it does, or skip if a file is missing. They don't make any suggestions how these files are created. And you definitely don't want to provide URLs for that files, which will trigger HTTP request on each image and slow down the process very badly. This is a non-issue for Image Styles, because they provide two public APIs:

    • \Drupal\image\ImageStyleInterface::buildUri() โ€” build URI, but file might be missing. This is basically how is now CSS/JS aggregation works.
    • \Drupal\image\ImageStyleInterface::createDerivative() โ€” generates an image derivative on request. Calling ::buildUri() after that will lead to already existing image processed by Drupal.

    So, new CSS/JS aggregation system basically lacks of ::createDerivative() analogue. The main logic is in \Drupal\system\Controller\AssetControllerBase::deliver().

    I think \Drupal\Core\Asset\AssetOptimizerInterface should require a new method implementation that will generate that files on demand.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom catch

    So, new CSS/JS aggregation system basically lacks of ::createDerivative() analogue. The main logic is in \Drupal\system\Controller\AssetControllerBase::deliver().

    I think \Drupal\Core\Asset\AssetOptimizerInterface should require a new method implementation that will generate that files on demand.

    I think this would be a good new issue to open.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom adamps

    That sounds like a good approach.

    Is there any way to avoid contrib code needing to test on Drupal version to decide whether to call the new method? Could we add a null version of the new method to older core versions??

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom longwave UK

    We can't retrospectively fix older versions that have already been released, so you would still need some checks. The safest way is to add method_exists check I think.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom catch

    Re-titling, I think we can re-purpose this issue.

    So: add a new method, which does most of the same logic as AssetControllerBase::deliver() (but doesn't check against the actual request, because it's for situations where you don't have a request or don't want to rely on it, like in Symfony Mailer).

    It would be something like

    function createAggregate(string $file_name, string $asset_type, array $query): string {}
    

    $query is probably the results of $query->All() and we check against the array instead of using $query->has() - that way calling code that's not the controller can use parse_url to get the query string to pass in.

    We can't add a new method to AssetCollectionOptimizerInterface because it both doesn't have a base class and isn't 1-1. Unless we add a base class, which maybe we could do, but we'd also need to add that to the two deprecated implementations or implement the new method on those too.

    The other option is a new service with a new interface with a single method.

    Either way we can use this service in AssetControllerBase so it'll be used by core.

    Although the method is called createAggregate, it'll check for the file if it already exists to avoid creating it again.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom adamps

    Here's a summary of my understanding:

    This issue affects any module that uses theming outside of the HTML context. That includes for emails, and could also include for PDF. It affects swiftmailer and symfony_mailer which already means 50k sites.

    Apparently there's no way to disable the new aggregation, so these modules currently don't work at all with D10.1. The modules need to change their code, however the new function they need to call doesn't exist yet.

    Problems:

    1) Documentation/communication: I can't find much indication of this problem in either the release notes or the change records.

    2) Action needed: Support for D10.0 ends on December 11th. Before then we need this patch to land, then time for the contrib modules to adjust their code and create a new release. This issue is apparently unassigned with nobody working on it. Can we make it a D10.2 release blocker? Otherwise 50k sites are left stranded with no version of Drupal that's usable and secure.

    Thanks๐Ÿ˜ƒ

  • heddn Nicaragua

    A work around for now in 10.1 (for those stumbling on this) is to set the css/jss library with a preprocess: false. That then means the file can be found in its un aggregated form. Not suggesting that's a good or bad solution. But it does seem to work in the mean time.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom adamps

    Actually it's not as many as 50k sites because it's only those that add CSS to the email. Maybe 10k sites?

    Seeing as this is an API change then I guess the deadline for 10.2 is the first beta release, Week of November 13, 2023.

    A work around for now in 10.1

    Thanks. Are there any possible disadvantages? Do you know if we can still do optimization to process @import?

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom catch

    Seeing as this is an API change then I guess the deadline for 10.2 is the first beta release, Week of November 13, 2023.

    If this was still not fixed by then and blocking contrib, we'd probably commit it during beta still, maybe not during rc.

    I think the main question here is where to put the new method per #3325118-11: Add an API method to get the contents of a CSS or JavaScript aggregate from a URL โ†’ . Another option I didn't think of when writing that comment was adding a public non-interface method to Css/JsAssetCollectionOptimizerLazy - contrib would need to do method_exists() but that'll be the case for a while even if we were to add a new interface method.

  • Status changed to Closed: works as designed over 1 year ago
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom catch

    I started looking at adding a helper service which would take the information from the URL and write out a file, and I think it is the wrong approach.

    The controller is very specifically dealing with a URL - i.e. it has to get information from the request and verify it, mock the active theme etc., this is because we don't have control over who visits the URL. It also has to do a load of work to parse out the libraries etc. from the URL due to compression etc. it's very, very specific to the fact we have to pass all that information in a URL, the actual code to build the aggregate from the libraries is minimal and using services that core already provides.

    The original code in Symfony mailer is building a URL in order to get the contents from it, which is not an API as such but relying on the behaviour, however it already has access to the libraries it wants to get assets for in the first place, which is not the case for the controller at all.

    On the other hand, #3371042-24: Drupal 10.1.0 new aggregation breaks InlineCssEmailAdjuster โ†’ uses core's existing API methods to get the CSS contents from the libraries, and this would have been the correct approach in the first place for earlier Drupal core versions too. The only reason to build a helper to get those contents from a file, would be essentially to use the file as a cache, but since it's the contents of the file that are needed, not the file itself, Drupal's cache system will work fine for that.

    I'm therefore marking this by design and will update the other issue.

  • Status changed to Active over 1 year ago
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom adamps

    @catch The problem hasn't gone away so I'm re-opening this and will reference it from the Drupal Symfony Mailer release note. Contrib modules such as Drupal Symfony Mailer need an efficient way to get CSS from a library. Prior to ๐Ÿ› Stampedes and cold cache performance issues with css/js aggregation Fixed , such a way existed, using file_get_contents.

    I understand your point that this was "relying on the behaviour", and that as such it might change in a minor version. However I would have thought that in this case it was very important to have a release note informing contrib code, showing the old code and replacement code. I have check the 3 linked release notes and as far as I can tell this didn't happen.

    If someone had tried to write this release note it would have revealed the more serious problem: there is apparently no equivalent simple code available.

    ๐Ÿ“Œ Drupal 10.1.0 new aggregation breaks InlineCssEmailAdjuster Needs review has made a fix using this commit. The new code doesn't include any caching, which will create a performance decrease for bulk sending, such as large newsletters. We don't yet have any data however I'm just creating a beta release so hopefully people will report back soon.

    In #11 you changed the title to "Add an API method to get the contents of a CSS or JavaScript aggregate from a URL". I trust your expert view in #16 that this actually isn't the correct approach, and instead it belongs in Drupal's cache system. I'm happy to alter Drupal Symfony Mailer for any usable API that Core might provide.

    this would have been the correct approach in the first place

    Drupal's cache system will work fine for that.

    You seem to be implying that the contrib code was wrong in the first place. However the only alternative I can see is to attempt to duplicate the entire Drupal Core CSS caching algorithm in contrib. Therefore I expect that 99% of developers would also have written the same "wrong" code๐Ÿ˜ƒ.

    Already the code after #3371042 is slightly more complex (8 lines rather than 2), duplicating the internal logic in CssOptimizer::optimize(). If we were to add caching then it would likely be more like 80 lines of complex code that is performance critical, possibly security-sensitive and easy to get wrong. We need to calculate a valid unique cache key. We would have to ensure that caches are invalidated when versions change (including complex edge cases such as ๐Ÿ› Ensure that edge caches are busted on deployments for css/js aggregates Fixed that even Core took several months to figure out) and when caches are manually cleared. And such code would be nearly impossible to maintain as already we have clearly seen that Core can change it substantially in a minor release. Contrib code needs to work with a range of minor releases, so it's just not going to work well.

    I have no interest in maintaining such code in Drupal Symfony Mailer๐Ÿ˜ƒ. If @catch decides to close this issue again, then I guess anyone who has a site that requires it should create their own contrib module. That module could easily implement a Drupal Symfony Mailer plug-in to operate without needing any changes to Drupal Symfony Mailer.

  • Status changed to Postponed: needs info over 1 year ago
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom catch

    You don't need to worry about edge cases like browser and edge caching of URLs because you're generating CSS for emails, not a URL. Using the cache API will result in invalidation when caches are cleared, which is enough.

    So maybe 10-15 lines of code, not 50.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom adamps

    Thanks for the reply @catch. I don't understand what more info you are looking for.

    AFAICS we do need to worry about the version field and the special significance of the value -1. Perhaps if you or another expert could write the "missing" change record including the correct lines of replacement code then it would help.

    Even then, you didn't respond to the matter of maintainability. Would you be willing to make a commitment to the suggested code remaining correct and complete during the lifetime of a major version?

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom catch

    @AdamPS

    AFAICS we do need to worry about the version field and the special significance of the value -1.

    Using the Drupal cache API would mean any full cache clear will definitely purge all of the cache entries - this is different to core's actual asset aggregation where we also need to worry about browser and edge caches we don't have control over.

    If you want to support cache-clear-free deployments, you can use AssetGroupSetHashTrait::generateHash() - need to add it as a trait on an existing class of course. Since this API exists, I'm not sure why you're asking for another one, hence needs more info.

    Even then, you didn't respond to the matter of maintainability. Would you be willing to make a commitment to the suggested code remaining correct and complete during the lifetime of a major version?

    If you use actual core interfaces, then the code will be supported for a major release (give or take deprecations, but it'll still work even if deprecated). If you use side-effects or behaviour details of code, then it's not.

    If you actually try to use the core APIs, and then are unable to do something, then it is valid to ask for one to be added, but instead of this the issue is mostly asking to add a replacement for what was an undocumented non-API, and we have enough API surface with multiple interfaces and multiple classes within the asset aggregation system already.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom adamps

    Thanks @catch.

    Yes I already understand the point you clearly made several times - that Core was entitled to make this change according to the BC policy. In many similar cases I have seen a change record explaining that sites needed to change their code, and how to do so. In this case such a thing doesn't exist, so there is lack of clarity in how to proceed. If you prefer to close this issue then fine.

    Personally speaking I am unable to dedicate any more time to working out the correct code. If there are sites with big newsletters then presumably they also have experienced developers who can do so, and hopefully share the results somehow.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom adamps

    I would appreciate any maintainer feedback on the following example please, and whether this is sufficient info to re-open the issue. Even the code we already have in Drupal Symfony Mailer (without caching) seems to depend on the internals of Drupal Core:

          if (($asset['type'] == 'file') && $asset['preprocess']) {
            // Optimize to process @import.
            $css .= $this->cssOptimizer->optimize($asset);
          }
          else {
            $css .= file_get_contents($asset['data']);
          }
    

    The if test was required to avoid an exception. However there's no mention of this restriction on the function comment, which just says "Optimizes an asset." It seems that in future a minor Core version could change the details of which assets were optimised or not, for example (and I'm completely making this up obviously) it might decide to optimise external assets that were marked with a new flag static (guaranteed not to change). I suggest that if the asset cannot be optimised then this function should simply return the file contents. In this case, the optimised asset is equal to the original asset.

    I also wonder if it's right that only file assets can be optimised. I realise that the CSS aggregation would only want to optimise file assets, but other code using the service might be different. If a site wanted to inline CSS that was external and contained @import that's exactly what it would need. Perhaps the result shouldn't be cached but that's a different matter - this function is about optimising not caching.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom catch

    That exception has been thrown since 2013, you're right that the interface is missing @throws, a documentation issue to add that would be good, it's separate from adding new API methods here.

    Removing the exception could be worth discussion, but that should be done in a separate issue too.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom adamps
  • ๐Ÿ‡ฆ๐Ÿ‡ทArgentina anairamzap Buenos Aires

    Just a comment to say we are experiencing this (css/js assets not present when using aggregation) when using AMP contrib module.

    I have already related the issue ๐Ÿ› AMP prevents css assets from generating when aggregation is enabled on Drupal 10.1 Needs review but I wanted to also let everyone here know that we have another failed case with a contrib module, maybe not so popular as symfony_mailer, but still 2.5k users might be affected by this (if they enable aggregation).

    Hopefully the idea of adding a new method to be able to generate the aggregated files is reconsidered.

    Please note that the patch attached in https://www.drupal.org/project/amp/issues/3374942#comment-15152404 ๐Ÿ› AMP prevents css assets from generating when aggregation is enabled on Drupal 10.1 Needs review tries to use the guzzle request approach (mentioned in https://www.drupal.org/project/symfony_mailer/issues/3371042 ๐Ÿ“Œ Drupal 10.1.0 new aggregation breaks InlineCssEmailAdjuster Needs review ) , but that's failing in too many scenarios:

    • Local docker env
    • Sites behind a proxy
    • Sites requiring a cookie to grant access

    Since we need amp for one of our client's site I'll be working on the related issue trying to add a workaround that ideally doesn't disable the aggregation for amp assets, but if we can not achieve that we will need to simply ignore aggregation in order to get amp pages working (even with a bad performance result).

  • ๐Ÿ‡ฆ๐Ÿ‡นAustria mvonfrie

    I just got the following warning after upgrading a site from 9.5 to 10.2:

    Warning: file_get_contents(themes/custom/my_theme/../../../../../modules/contrib/colorbox/styles/default/colorbox_style.css): Failed to open stream: No such file or directory in Drupal\Core\Asset\CssCollectionOptimizerLazy->generateHash() (line 43 of core/lib/Drupal/Core/Asset/AssetGroupSetHashTrait.php).
    Warning: file_get_contents(themes/custom/my_theme/../../../../../modules/contrib/colorbox/styles/default/colorbox_style.css): Failed to open stream: No such file or directory in Drupal\system\Controller\CssAssetController->generateHash() (line 43 of core/lib/Drupal/Core/Asset/AssetGroupSetHashTrait.php).

    I can fix that by patching the colorbox.libraries.yml file like this:

    plain:
      version: VERSION
      js:
        styles/plain/colorbox_style.js: {}
      css:
        theme:
    -      styles/plain/colorbox_style.css: {}
    +      styles/plain/colorbox_style.css: { preprocess: file }
      dependencies:
        - colorbox/init

    But that doesn't really solve the issue here. The Drupal site is located at /var/www/html/ (web root is at /var/www/html/web/) which means that the ../../../../../

    resolves to /var/www/modules/contrib/colorbox/styles/default/colorbox_style.css
    instead of /var/www/html/web/modules/contrib/colorbox/styles/default/colorbox_style.css
    and of course that file doesn't exist. I'm not sure whether this (the ../../ too much) is a bug in core or in the colorbox module. Therefore I didn't create an issue for that yet.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom catch

    colorbox should probably use public://styles/default/colorbox_style.css

Production build 0.71.5 2024