Make language switcher block cacheable

Created on 3 April 2014, about 11 years ago
Updated 13 February 2023, over 2 years ago

Postponed on #2453059: Set default render cache contexts: 'theme' + 'languages:' . LanguageInterface::TYPE_INTERFACE and 🌱 Optimize render caching Needs work .

Problem/Motivation

The language switcher block is currently not cacheable at all.

Proposed resolution

If we have a "language redirect" route, i.e. /language-redirect/<language code>, then e.g. "français" would link to /language-redirect/fr?destination=node/42. Then everything except the destination parameter could be cached globally. The destination parameter could be set in JS, just like contextual.js already does today:

    // Set the destination parameter on each of the contextual links.
    var destination = 'destination=' + Drupal.encodePath(drupalSettings.path.currentPath);
    $contextual.find('.contextual-links a').each(function () {
      var url = this.getAttribute('href');
      var glue = (url.indexOf('?') === -1) ? '?' : '&';
      this.setAttribute('href', url + glue + destination);
    });

To remain JS-less for anonymous users, we could still generate the entire block dynamically for anonymous users, because Drupal assumes that anonymous users get served cached pages anyway. This would then be in line with what we do for active link handling: we set the active class in PHP (on the server side) for anonymous users, but in JS (on the client side) for authenticated users.

That's the best of both worlds.

Remaining tasks

Blocked on #2107427: Regression: Language names should display in their native names in the language switcher block and #2335661: Outbound path & route processors must specify cacheability metadata and 🌱 Optimize render caching Needs work .

User interface changes

None.

API changes

None.

📌 Task
Status

Postponed

Version

10.1

Component
Language system 

Last updated about 8 hours ago

  • Maintained by
  • 🇩🇪Germany @sun
Created by

🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

Live updates comments and jobs are added and updated live.
  • D8MI

    (Drupal 8 Multilingual Initiative) is the tag used by the multilingual initiative to mark core issues (and some contributed module issues). For versions other than Drupal 8, use the i18n (Internationalization) tag on issues which involve or affect multilingual / multinational support. That is preferred over Translation.

  • Performance

    It affects performance. It is often combined with the Needs profiling tag.

Sign in to follow issues

Merge Requests

Comments & Activities

Not all content is available!

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

  • 🇪🇸Spain pcambra Asturies

    Just an aside comment, this patch wasn't working for me OOTB, and what I had to do is in a custom block extending the LanguageBlock one.

    Rather than using the render block, use the getCacheContexts method:

      /**
       * {@inheritdoc}
       */
      public function getCacheContexts() {
        return Cache::mergeContexts(parent::getCacheContexts(), [
          'user.permissions',
          'url.path',
          'url.query_args',
          'languages:language_content',
          'languages:language_interface',
        ]);
      }
    

    And rather than removing the getCacheMaxAge method, override it by:

      /**
       * {@inheritdoc}
       */
      public function getCacheMaxAge() {
        return $this->cacheMaxAge;
      }
    

    You need to include use CacheableDependencyTrait; in your custom block.

  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update almost 2 years ago
    Composer error. Unable to continue.
  • last update almost 2 years ago
    30,335 pass, 1 fail
  • Status changed to Active 7 months ago
  • 🇳🇱Netherlands bbrala Netherlands

    This is blocked on 🌱 Optimize render caching Needs work curretnly. But im not sure thats needed. Perhaps we can find another solution that allows us to fix this issue. That would unblock 🐛 [pp-3] Bubbling of elements' max-age to the page's headers and the page cache Postponed more, and i feel like the whole render cache issue is a bit to big and complicated.

  • 🇮🇳India samit.310@gmail.com

    samit.310@gmail.com made their first commit to this issue’s fork.

  • Pipeline finished with Failed
    7 months ago
    Total: 1220s
    #305885
  • Pipeline finished with Success
    7 months ago
    Total: 409s
    #305937
  • Status changed to Needs review 3 months ago
  • 🇨🇭Switzerland berdir Switzerland

    Many years later, there's actually progress here.

    📌 Review cache bin and cache tags of access policy caching Active introduced a CacheoOptionalInterface, which is very close to what I've imagined back in 2016.

    Implemented that and made BlockViewBuilder respect that.

    The result of that is interesting.

    --- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php
    +++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php
    @@ -51,18 +51,18 @@ public function testFrontPageAuthenticatedWarmCache(): void {
    
         $expected = [
           'QueryCount' => 4,
    -      'CacheGetCount' => 40,
    +      'CacheGetCount' => 34,
           'CacheGetCountByBin' => [
    -        'config' => 22,
    -        'discovery' => 5,
    -        'data' => 7,
    +        'config' => 18,
    +        'discovery' => 4,
    +        'data' => 6,
             'bootstrap' => 4,
             'dynamic_page_cache' => 2,
           ],
           'CacheSetCount' => 0,
           'CacheDeleteCount' => 0,
           'CacheTagChecksumCount' => 0,
    -      'CacheTagIsValidCount' => 10,
    +      'CacheTagIsValidCount' => 9,
           'CacheTagInvalidationCount' => 0,
           'CacheTagLookupQueryCount' => 5,
           'ScriptCount' => 1,
    

    On HEAD, LanguageBlock has a max-age 0 and gets placeholdered. That means on a dynamic page cache hit on umami, we need to load the block + language including translation overrides, block plugins and load routes. With this, the block isn't cached on its own, so we have one cache get less, but it's just part of the dynamic page cache. The downside of this is that we end up with url.query_args cache context in dynamic page cache. That didn't seem to cause any test fails so far, so we don't seem to have explicit test coverage for the cache contexts on dynamic page cache on a multilingual site with language switcher. Earlier attempts in this issue attempted to move thing into javascript. maybe we could just do that with the query string, unsure, or we placeholder just the query string or something like that.

  • Pipeline finished with Success
    3 months ago
    Total: 901s
    #414100
  • 🇬🇧United Kingdom catch

    placeholder just the query string

    That sounds like a good thing to start with.

  • 🇨🇭Switzerland berdir Switzerland

    It's easier said than done though. We have placeholders for single query string arguments, but this is different. Note that some language switch links can also add a langcode parameter, so it needs to deal with appending to that as well. maybe the whole URL could be a parameter for the placeholder? seems awkward.

    It's also something that tends to be heavily customized/themed (icons, shortened languages, all kinds of completely custom designs), there's an alter hook and specific template suggestions for a link template. There's a fairly high chance that we'll break those customizations if we no longer pass in what we do now.

  • 🇬🇧United Kingdom catch

    Oh I didn't realise it can have two query strings to individual placeholder and which also aren't guaranteed to exist, that does indeed sound painful.

    We can set #create_placeholder explicitly on the whole block, per 📌 Create placeholders for more things Active . And then 📌 Render the navigation toolbar in a placeholder Active will mean it's no-longer rendered via big pipe when it's cached. That will keep the cache contexts isolated from the main dynamic_page_cache entry then but it'll still be render cached.

  • 🇨🇭Switzerland berdir Switzerland

    Sounds good, I think it makes sense to wait on those two issues then.

    Note: views also adds query_args as cache context to any view due to filters, sort and so on, realized that in the common cache tags issue with the new test. I was recently wondering on a project why dynamic page cache didn't kick in on a project where a bot (GPTBot) sent tens of thousands of requests that only varied in some tracking query string, that would be why). Might have some potential to limit dynamically based on having exposed filters/sorts/pager.

  • 🇬🇧United Kingdom catch

    I don't think we need to wait on 📌 Create placeholders for more things Active , we can just copy the same pattern from that issue to here for the language switcher block. That issue has taken various twists and turns when I realised what it does and doesn't solve (completely different things to when I opened it), but the code changes are minimal/trivial. We don't necessarily need to wait for the cached placeholder strategy either although it will probably result in some test churn in either direction.

    Seems like a good issue to open against views to add that conditionally.

  • 🇺🇸United States smustgrave

    The one out of scope change if it's being suggested it fixes 🐛 Big Pipe, logged-in users and 4xx pages Active then think test coverage here should be expanded to include that ticket scenario too. And if that's the case we can close the other issue and move over credit.

    3349201 also mentions once it's solved 🐛 Requests are pushed onto the request stack twice, popped once Needs work would be solved too so curious if this hits 3 issues actually?

  • Pipeline finished with Failed
    about 2 months ago
    Total: 151s
    #454404
  • 🇨🇭Switzerland berdir Switzerland

    Trying to go back to keeping it cacheable but using the new forced placeholder ability. That kind of has the same benefits to cache, although that's mostly FastChained lookups anyway. twig debug shows that it's pretty fast, but it can get more complex with multiple languages I suppose.

    I could also experiment with CacheOptional + placeholder and maybe also skipping big pipe (

    @smustgrave: We're not fixing those issues. This just changed the render context/time so we're not longer applied by it. In that mode, it also can't be reverted, because that was the whole point of this change. That said, the new placeholder approach might again change how this works, we'll see, didn't run that test.

  • Pipeline finished with Failed
    about 2 months ago
    Total: 531s
    #454410
  • Pipeline finished with Canceled
    about 2 months ago
    Total: 425s
    #454421
  • Pipeline finished with Success
    about 2 months ago
    Total: 362s
    #454425
  • Merge request !11571Resolve #2232375 "Make language switcher nocache" → (Open) created by berdir
  • 🇨🇭Switzerland berdir Switzerland

    Created a second MR that uses a forced placeholder and the CacheOptionalInterface. This makes it kind of similar to HEAD, no impact on performance tests, but unlike max-age 0, this doesn't bubble up and shouldn't break page caching once that looks at that. It could maybe also use 📌 Allow the messages block to skip placeholdering Active once that's in, but would require an alter hook I think to set it on the right level.

  • Pipeline finished with Failed
    about 2 months ago
    Total: 536s
    #454798
  • Pipeline finished with Success
    about 2 months ago
    Total: 646s
    #455341
  • The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

    This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

    Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

  • 🇨🇭Switzerland berdir Switzerland

    Rebased.

  • 🇨🇭Switzerland berdir Switzerland

    Leaving at needs work, need to update the issue summary with the options and questions.

  • Pipeline finished with Success
    about 2 months ago
    Total: 441s
    #459085
  • Pipeline finished with Success
    about 2 months ago
    Total: 452s
    #459088
  • 🇨🇭Switzerland berdir Switzerland

    Issue summary updated

  • 🇬🇧United Kingdom catch

    🐛 Page cache creates vast amounts of unneeded cache data Active is getting more attention recently (maybe more bad crawlers around?), and the dynamic_page_cache not varying by query parameters unless it actually has to is one of the mitigations for that issue. Both varying the dynamic page cache by query params, or introducing a separate cache item that does, would make that particular issue worse for people I think. So for me that leads strongly towards option #c.

    I did think of one other possible approach when reading the IS:

    What if we made a special placeholder (similar to CSRF placeholder handling) for only the query string and replace only that? Then we could remove the query string cache context but handle it all in PHP still.

    This also made me wonder how bad would it be if we dropped the query string entirely - usually when I change language on a site I'm either testing, or it's the first thing I do when I land on the site, not 5 pages into a pager (which might not show the same results anyway). But probably if we did that someone would be relying on it somewhere.

  • 🇨🇭Switzerland berdir Switzerland

    As quickly discussed on slack, changing how the links are build is challenging because it's a pluggable system depending on your language negotiation settings. See \Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl::getLanguageSwitchLinks, session/cookie based language adds its own query parameter that it then uses on the given request to put it back into a cookie. It's not impossible but tricky with BC and relying on this happening and so on.

    FWIW, I'm reasonably certain that building this is faster than fetching from cache, based on renderering time debug output (had to hack it to show times also for non-cacheable elements), it's also the same right now in HEAD. But there might indeed be edge case such as it being the only thing it has to render with twig.

    I also verified that this fixes OpenTelemetryFrontPagePerformanceTest.php with 🐛 [pp-3] Bubbling of elements' max-age to the page's headers and the page cache Postponed , which currently fails as this block prevents it from being a page cache hit.

    It's also no change compared to HEAD. The block is currently being autoplaceholdered and not cached as well, just also in a way that that information up.

    I also created 📌 Add CacheOptionalInterface to more blocks Needs review as a follow and want to add a change record for this.

  • 🇨🇭Switzerland berdir Switzerland
  • 🇨🇭Switzerland berdir Switzerland

    Attempt at a new title

  • 🇨🇭Switzerland berdir Switzerland
  • 🇨🇭Switzerland berdir Switzerland

    berdir changed the visibility of the branch 2232375-make-language-switcher to hidden.

  • Pipeline finished with Success
    about 2 months ago
    Total: 6427s
    #461893
  • Status changed to Needs work 20 days ago
  • 1 small nit on MR11571.

    And this is a question to make sure I understand the render system correctly, not really a concern:

    The language switcher block is not cached on its own, but it is placeholdered with the appropriate cache metadata. Assuming the block is placed globally within a region, because of the placeholdering, the cache contexts do not bubble to dynamic page cache entry, and the cached markup in the dynamic page cache includes the non-replaced placeholder markup.

    But if the language switcher blocked were placed in a node layout via layout builder, or rendered inside another block template with twig_tweak or similar, then the node or wrapping block could be rendered cached with all the variations for query strings, etc.? The difference being that in HEAD, with max-age 0 being bubbled, the node or wrapping block would not be cached.

  • 🇬🇧United Kingdom catch

    @godotislate #107 is exactly right, however there's an existing layout builder issue to address this, see 🐛 Not possible to use render placeholdering with Layout Builder blocks Needs work . I've also opened an equivalent issue for experience builder here more recently: 🐛 Placeholders/#lazy_builder is not supported for block component rendering Active - so for me these are bugs in those modules not fully supporting the block plugin API.

  • @catch Thanks for those issue links. And yeah, my question is not a blocker here.

    One thing about this issue, though, is that maybe there should be at least 1 if not 2 tests?

    1. A test that any block with a plugin implementing CacheOptionalInterface is not render cached
    2. A test that a page with the language switcher block on it will be cached in the dynamic page cache

    The first one at least since an API change is being made to BlockViewBuilder.

  • Pipeline finished with Success
    11 days ago
    Total: 877s
    #488727
  • Added the first test mentioned in #109.

  • Pipeline finished with Success
    11 days ago
    Total: 1357s
    #488737
  • 🇨🇭Switzerland berdir Switzerland

    Thanks for 1, makes sense, I added one small note on that.

    For 2, it's not 100% explicit on the intent, but we do have performance tests that explicitly assert the page cache and dynamic cache page on umami, which is multilingual with language switcher.

    We no longer have any changes in those tests because the behavior for them is unchanged in regards to the language switcher block, but earlier merge requests had changes there.

  • Pipeline finished with Success
    10 days ago
    Total: 584s
    #489197
  • Made change to test based on feedback.

  • Pipeline finished with Success
    10 days ago
    Total: 437s
    #489327
  • Pipeline finished with Success
    10 days ago
    Total: 604s
    #489400
  • 🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium

    Quoting option B:

    B) Use the new \Drupal\Core\Block\BlockPluginInterface::createPlaceholder() to force this to be a placeholder and cache it separately. In our projects, we're seeing 10k+ variants of this block, very rarely reused. That's MR !9798.

    However, that MR seems to also involve CacheOptionalInterface? And I'm not sure we want to be using that here.

    My reasoning is this: CacheOptionalInterface was introduced with access policies, where multiple smaller pieces will be cached together as a whole. In that context the interface meant: "If all of the smaller pieces indicate they are insignificant, we need not cache the whole."

    However, render arrays with cache keys (including the language switcher block) aren't necessarily like that. They get cached individually and as part of a whole (DPC). So using the interface there seems ambiguous as it's not clear in which caching layer they would be considered optional.

    Now, with that in mind, LanguageBlock should already be auto-placeholdered by virtue of its max age being 0 and BlockViewBuilder making it use a lazy builder. So setting createPlaceholder() should effectively change nothing about the current situation. The block will be placeholdered and DPC will not cache it.

    So it seems the goal is to rid LanguageBlock if its max age of 0 and then make sure it doesn't poison the page. That part is not fully clear from the issue title and summary.

    In that case, option B seems like the most straightforward fix, but we would indeed not want to individually cache all variations of LanguageBlock because it varies too much. So removing it's max-age 0 is a good thing, as it would otherwise kill all page caching when combined with 🐛 [pp-3] Bubbling of elements' max-age to the page's headers and the page cache Postponed .

    Then again, if we merely want to placeholder it and we don't want the placeholdered HTML to be cached, we have more options than to resort to max age 0. I'm wondering if this was ever explored. Let's look at renderer.config:

    renderer.config:
    required_cache_contexts: ['languages:language_interface', 'theme', 'user.permissions']
    auto_placeholder_conditions:
    max-age: 0
    contexts: ['session', 'user']
    tags: []
    debug: false

    As you can see, we could achieve the very same result of having LanguageBlock auto-placeholdered by providing a cache tag that indicates the desired behavior. Much like the special "CACHE_MISS_IF_UNCACHEABLE_HTTP_METHOD:" cache tag we have. This tag would also bubble up to FinishResponseSubscriber, but it would have zero effect on the max-age of the page.

    So rather than use CacheOptionalInterface in a way that might confuse people, why don't we introduce a "SHOULD_AUTO_PLACEHOLDER_BUT_NOT_CACHE" cache tag, add it to LanguageBlock and renderer.config.auto_placeholder_conditions.tags?

    Then all we'd have to do is adjust RenderCache a bit to also check for that cache tag and we'd have a system that works for ALL render arrays (with a lazy builder) rather than having to figure out ways to apply an interface to them.

  • 🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium

    Just adding this: I like how clean the current MR looks and I can see the reasoning behind it. But flagging the LanguageBlock as "cache optional" does not cover the load here. Because we want it to not be cached, ever. So it's hardly optional. Furthermore, we also want to placeholder it. And I can see that (placeholder & no cache) being the case for more pieces of content in the future.

    Hence my suggestion for something that would both indicate we do not want to cache it and we want to placeholder it.

  • 🇨🇭Switzerland berdir Switzerland

    The autoplaceholder config is config. We can change the defaults, but it wouldn't really apply to all existing sites that have customized this. IMHO that makes this very hard to control and those sites will then have negative effects from not having that.

    I also think that cache tag bubbling is confusing with that special cache tag. I'm not exactly sure about exact behavior, but it seems unpredictable. As we learned recently, before Optimize placeholder retrieval from cache Active , placeholdered elements were still included if their element was already render cached, there might be other unexpected side effects.

    Also, autoplacholdering doesn't disable caching like max-age 0 does, it would still be cached on its own.

    It might not have been clear in the issue where we added it, but I already had this issue in the back of my head to use it here as well, that's why I moved it to the cache namespace and made it more generic. We should extend the docs to mention block plugins as well. In my mind, it's less about supporting CacheOptionalInterface in render arrays (we don't), it's supported specifically for block plugins and makes them not cacheable in the initial definition, but that's an implementation detail of BlockViewBuilder. Other places that support block plugins, such as layout builder, twig or block_field do not currently support per-block caching or placeholdering at all. Which, to reply to #107 as well, is IMHO OK, because it says "cache optional", not "must not be cached". In other words, "not worth caching on its own", which I think fits the use case in access policies.

  • 🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium

    Also, autoplacholdering doesn't disable caching like max-age 0 does, it would still be cached on its own.

    Which is why I added:

    Then all we'd have to do is adjust RenderCache a bit to also check for that cache tag

    But aside from that detail, I agree that making this rely on config might not be such a great idea. Your further clarifications I also tend to agree with. The suggested implementation here is one of "do not cache", but it doesn't mean that other implementations have to go down that same path.

    Okay in that spirit I'll go over the MR again, but I would cautiously agree with the approach. We need to be very aware of #108, though. Ideally those issues get fixed sooner rather than later so the changes introduced here don't run havoc there, even if it's technically not "our fault" if it does.

  • 🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium

    Two remarks, but I don't see why this MR can't be RTBC once they are discussed/resolved.

    Leaving at NR as there are no actionable items, yet. Depends on where the discussion leads us.

  • I think this is at NW based on latest comments.

Production build 0.71.5 2024