Implements cache for tailwind output

Created on 19 November 2024, 4 months ago

Problem/Motivation

Tailwind JIT works nicely with Internal Cache Page module. But when there're dynamic query parameters in the request, the internal cache will consider it as a new request, hence trigger the tailwind compiler. However, page content might still be the same with different query params, e.g ?utm=tracking_source, so unnecessary recompilation bring high pressures to the server.

Proposed resolution

1. Extract the css classes from the content
2. Sort the css classes with alphabet
3. Generate a cache key with classes hash
4. Cache tailwind's output, so next time when the same set of classes are requested, return the cache result immediately

It can also benefit authenticated visitors, since it can work without internal page cache.

Works to be done

Right now only element classes are extracted. Classes in javascript or json string are not handled.

Feature request
Status

Active

Version

1.0

Component

Code

Created by

🇨🇳China stjuan627

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @stjuan627
  • Status changed to Needs review about 2 months ago
  • 🇳🇿New Zealand garethhallnz

    I too have run into a similar issue; for me most of out users are authenticated and Drupal's internal page cache module and even the dynamic page cache module does very little to help. Reverse proxy cache like varnish will obviously help but not all sites have access to such systems.

    I have built on top of @stjuan627 patch and expanded on his work.

    Why This Feature is Needed

    During my testing, I observed that while Drupal’s internal page cache works well for anonymous users, performance for authenticated users suffered significantly. On average, page load times for authenticated users were 2-3 seconds slower due to the lack of caching for compiled Tailwind CSS. This is because authenticated users often bypass Drupal’s internal page cache, leading to repeated CSS compilation for every request.

    The new caching feature addresses this issue by:

    Improving Performance for Authenticated Users:
    - By caching compiled CSS, authenticated users no longer experience the overhead of repeated CSS compilation, resulting in faster page load times.
    Enhancing Performance for Anonymous Users:
    - While anonymous users already benefit from Drupal’s internal page cache, this feature adds an additional layer of optimization by caching compiled CSS separately. This reduces the time required to generate the final HTML response.
    Scalability:
    - High-traffic sites, especially those with a mix of anonymous and authenticated users, will see significant performance improvements, as the caching mechanism reduces server load and improves response times.

    New Features

    1. CSS Caching for Improved Performance

    I’ve added a caching layer to store compiled Tailwind CSS, significantly reducing the need for repeated compilation and improving response times for subsequent requests. Key highlights include:

    Configurable Cache Settings:
    - Enable or disable CSS caching via the theme settings form.
    - Set a cache lifetime, with options ranging from 1 hour to 90 days or permanent storage.
    - Choose a hashing algorithm for cache key generation (default: `xxh3` for optimal performance).

    Automatic Cache Invalidation:
    - The cache is automatically cleared when theme settings are saved or when the cache is manually cleared via Drupal’s cache administration page.

    Efficient Cache Key Generation:
    - Cache keys are generated based on the unique CSS classes found in your content and the active theme, ensuring accurate and efficient caching.

    2. Extraction of Unique CSS Classes

    To optimize CSS compilation, I’ve introduced a new method to extract and deduplicate CSS classes from your HTML content. This ensures that only the necessary CSS is compiled and cached, reducing overhead and improving performance.

    3. Enhanced Theme Settings Form

    The theme settings form has been updated to include new options for managing the caching feature:
    - A new section allows administrators to configure caching settings, including enabling/disabling caching, selecting a hashing algorithm, and setting the cache lifetime.

    Improvements

    Dependency Injection:
    - The `HttpMiddleware` class now injects the `CacheBackendInterface` service, enabling seamless integration with Drupal’s caching system.

    Code Refactoring:
    - Added helper methods (`injectStyleTag()` and `generateCacheKey()`) to streamline the caching and CSS injection process.

    Performance Optimization:
    - Reduced redundant CSS compilation by leveraging cached results, improving response times for high-traffic sites.

    How to Use the New Features

    1. Enable Caching:
      - Navigate to the theme settings form (`/admin/appearance/settings/[your_theme]`).
      - Under the "Experimental features" section, enable CSS caching and configure the cache lifetime and hashing algorithm.
    2. Monitor Cache Performance:
      - Use Drupal’s cache administration page (`/admin/config/development/performance`) to monitor and clear the cache as needed.
    3. Test and Optimize:
      - Test the caching feature in a staging environment to ensure compatibility with your site’s configuration.
      - Adjust the cache lifetime and hashing algorithm based on your performance and security requirements.

    Backward Compatibility

    This release is fully backward-compatible with existing configurations. However, the caching feature is marked as experimental, so I recommend thorough testing before enabling it in production environments.

    Bug Fixes

    - Fixed an issue where redundant CSS compilation could occur for repeated requests.
    - Improved error handling during CSS compilation to ensure graceful fallbacks.

  • 🇦🇹Austria hudri Austria

    Hello @garethhallnz and @stjuan627,
    sorry for not giving feeback for so long. I've been testing your patches now, and I'm really impressed by the performance gain. I've been testing it on my own sites, and the speed boost was massive on sites with complex Tailwind files. I really like the work done here and I think many users will greatly benefit from this feature!

    I'd like to reduce the interface a bit though. While the documentation is great, it might just be a bit to much for the average site admin, and the options seem to be a bit of academic nature, and not that essential in real operations.

    Is there really any need to let the user pick different hashing algorithms? Even for ancient MD5, where collisions are easy to craft, they rarely happen naturally in real world. Since this is not a security related hashing, I can't see any situation where I'd advice a site admin not pick the fastest option.

    And do we really need an additional cache lifetime setting here? Due Tailwind's atomic single responsibility classes, it is extremely unlikely that a cache hit will ever be outdated. Even if that edge case ever happens, a simple cache clear will solve the problem.

    I don't think that the extra UI complexity of choosing hash algorithms and cache lifetimes will lead to better performance or less
    problems in the wild. Therefore I think a simple on/off switch is enough.

    Do you see any problems with removing hash or lifetime options?

  • 🇳🇿New Zealand garethhallnz

    Thank you for your thoughtful feedback and for testing. I'm really happy to hear the performance improvements were noticeable!
    My thought are:

    Hashing Algorithm Selection
    I completely agree that hash collisions are not a real concern here. The reason I included multiple options was for compatibility. While xxh3 is significantly faster (around 30x faster than md5), it was only introduced in PHP 8.1. Since the module still supports Drupal 9 via 1.0.x, there are likely users running PHP 7.4 or 8.0 who wouldn’t have access to xxh3.

    Options:

    1. Automatically use xxh3 if it's available and fall back to md5 when it's not, without exposing this as a user-configurable option. This simplifies the UI while ensuring backward compatibility.
    2. Drop support for 1.0.x so we only need to support PHP 8.1+ and thus know that xxh3 is available.

    Cache Lifetime Setting
    The primary reason I added this was to allow flexibility for different caching strategies, particularly in setups using reverse proxy caches like Varnish. For example Lagoon documentation https://docs.lagoon.sh/applications/drupal/services/varnish/ explicitly recommends disabling Drupal’s Internal Page Cache when using a reverse proxy, which means relying on alternative caching mechanisms.

    In my own use case, I’ve found that authenticated users often benefit from different cache policies than anonymous users. That said, I see your point about Tailwind’s atomic nature making stale cache entries unlikely.

    I think this setting should remain different because if this module depends on the lifetime setting from the Internal Page Cache module then we really so set the dependency array in the info file.

    Options:

    1. Default to permanent caching (CACHE_PERMANENT) but allow developers to override it via hook_alter().
    2. Default to permanent caching (CACHE_PERMANENT), remove the cache lifetime setting from the UI but allow an override in settings.php

    I personally don't like the above and far prefer the current setup.

    Final Thoughts
    I completely understand wanting to keep the UI simple, and I agree that too many options can be overwhelming for site admins. However, I’d argue that most developers using Tailwind in Drupal are already fairly technical, given how much custom Twig and hook-based class injection is often required to make it work well.

    Ultimately, I was scratching an itch and didn’t test everything to death, which is why I marked it as experimental. My personal experience has taught me that caching can be a pain in the butt, so we should tread lightly until we are confident that our assumptions are true (Mostly mine :))

    That said, if you feel strongly about reducing the UI complexity, I think the best path forward would be:

    I would personally suggest creating a version 2 that drops support for 1.0.x. This way, we can ensure PHP 8.1 is used, allowing us to remove the hashing configuration. Since this module doesn’t depend on the Internal Page Cache (nor do I think it should), I believe having the cache lifetime setting in the UI is reasonable.

  • 🇦🇹Austria hudri Austria

    Thanks for your feedback. I think we should go settings.php then, because this way it is easy to create environment specific overrides there, e.g.
    $settings['tailwind_jit_cache_lifetime'] = $_ENV['SOME_VARNISH_SETTING'];

    I currently have some spare time and will try to implement this with a custom cache bin (which also has the side effect of getting a free override mechanism for the cache size)

  • Merge request !9Issue #3488357: Cache compiled CSS → (Merged) created by hudri
  • 🇦🇹Austria hudri Austria

    Created a new MR based upon the work of @garethhallnz and @stjuan627

    • caches compiled CSS in a custom database cache bin ==> cache size and lifetime can be overwritten in settings.php if necessary
    • caches HTML and Ajax requests
    • caches by class attributes (not individual class names, because .foo .bar is not the same as .foo.bar)
    • but class attributes are normalized for better cachability (sorted, normalized to single whitespace, filter core's .js-view-dom-id-XXX class)
    • UI simplified to single boolean toggle per theme
    • Update hook enables caching by default, as it will be useful and recommended for almost all users
    • added hook_uninstall()

    Hot-Link to Patch: https://git.drupalcode.org/project/tailwind_jit/-/merge_requests/9.patch

    We are currently testing this, if all goes well a new stable release is planned in one or two weeks.

    • hudri committed 1382e9cc on 1.0.x
      Issue #3488357: Cache compiled CSS in database
      
  • 🇦🇹Austria hudri Austria

    Published in release v1.2.0

Production build 0.71.5 2024