Updated: Comment #118
Problem/Motivation
HTML links generated by either l(), theme_links() or linkGenerator() all dynamically check whether they are currently "active" by comparing the current URL's structure against the parameters being used to construct the link tag. This "active" class is used very commonly by themers to provide visual cues for users navigating the site, and there is no "browser native" or other CSS equivalent - this functionality can't be achieved without modifying the DOM.
The problem is that we cannot cache these links with any better granularity than "per page". Ideally, we would want to be able to cache links globally as link generation functions can be called hundreds of times on each page load and most sites would get a decent cache "hit rate" as it's very common for the same link to be shown on multiple pages in a site.
This problem existed in Drupal 6 and 7, but there are two main reasons why it is becoming more important to resolve this issue in Drupal 8:
- Render caching is something that we're trying to support as much as possible in D8 core (it exists in D7 but is not widely supported "out of the box"). Any content containing a link "inherits" the per-page cache granularity restriction, even if that content would otherwise be globally cacheable. If the content wrapping the link is only cacheable per-user, we end up with a per-page+per-user limitation potentially due to a single link. #1830598: [meta] support render caching properly .
- The new routes system in Drupal 8 has been profiled a few times with respect to link generation, and while results are preliminary and hard to refine, the pattern seems to be that it is significantly slower than the soon-to-be-deprecated l() function has been historically. This puts pressure on core to avoid uncacheable links wherever possible if we're to stay competitive performance-wise with previous versions of Drupal. #2102777: Allow theme_links to use routes as well as href , #2047619: Add a link generator service for route-based links .
There is a very closely related issue with the "active-trail" class that is also applied to links, but not directly from within l(), linkGenerator() or theme_links(). This is also important to address but is out-of-scope here and the two classes should not be confused as they serve distinct purposes.
Proposed resolution
There are two proposed approaches to a resolution here. There was a third, CSS based approach presented in #64 and #92 but it hasn't gained much momentum as it introduces too many new problems/limitations, see #93.
Approach #1: Applying the active class via JavaScript
The idea is that either all links, or links with a Drupal-specific data attribute, are processed client-side and the server can simply ignore the "active" class altogether, allowing global cacheability by default. A "page ID hash" system was proposed in #67 that would require the server to "tag" each link with a hash that represents the URL, query parameters, language, etc... at this point JavaScript just has to match the current hash (likely also provided by the server) with any hashes on links in the DOM.
For the large majority of sites, the extra JavaScript wall time will be much lower than the rendering time saved on cached links, so we get to the "page loaded" event faster in absolute terms, and reduce server load at the same time.
Benefits:
- Guarantees global cacheability for all links.
- Unblocks future support for "active" class on "raw"
<a>
tags in Twig templates - something Twig initiative members and Core Maintainers have requested time and time again over the past few years - The "old" behaviour can be re-implemented in contrib relatively easily by removing the JS file adding "active" classes and implementing hook_link_alter()
- "active" class logic can be disabled globally pretty easily for sites that don't need it by removing the JS file
Consequences/Limitations:
- Requires javascript to work and therefore adds javascript to every page with at least one link on it. Comment #7, but note that the latest JS patches do not require jQuery as they are implemented with native JS only, as per #8.2
- For sites without javascript aggregation enabled: An extra http request will be required to load the js logic
- For sites without any other javascript at all: This would be the only javascript on the page, invoking the js parser/compiler, adding 100ms+ on mobile devices before the JS can be run at all.
- Potential "FOUC" issues, comment #63
Approach #2: "Opt-in" server-side active class logic
This approach adds an extra flag in the parameters passed to link generation functions that will determine whether Core bypasses the "active" class calculations. This means that some links will be globally cacheable and others will retain the per-page limit. Some examples of links that we can be reasonably sure either need or don't need the class can be found in comments #16 and #18.
Benefits
- Does not require JavaScript
Consequences/Limitations
- Makes an assumption that we know ahead of time what a themer will want to do with a link. Wherever we have uncertainty, we limit our cache granularity unecessarily or we limit what the themer can achieve.
- Introduces complexity and new API features into the system.
- All navigation HTML elements are still only cacheable per-page, but this content would benefit from global caching as it would have a high cache hit rate across different pages.
- Cannot be cleanly "swapped out" for a JavaScript solution in contrib.
- Continues to block "proper" usage of
<a>
tags in Twig templates. - It's unclear how the content containing a link determines whether it contains a link that has a "check active" flag set to TRUE or not. Likely it can't, and so this approach only unblocks global caching of more "hard coded" content.
Summary
The JavaScript approach is universal, simpler to maintain and more flexible, but will incur some client-side overhead.
The "opt-in" server-side logic has limitations that will likely prevent us from achieving as much as we would like in the area of render caching and introduces new complexity into the link generation API but does not introduce new client-side dependencies.
Remaining tasks
Profiling
Both of JS and the server-side logic. We have some numbers in #78 and #79, some discussion in #74 and #84.
So far, hypothetical numbers like "100-200ms" have been thrown around, unfortunately there is little hard evidence to discuss. The evidence provided actually showed a maximum of 25ms overhead from the JS - but there were no steps to reproduce, so it's hard to discuss this objectively - for example, does this 25ms represent 1 link in the page or 1000?
There is no XHProf profiling of the PHP from either of the JS or server-side approaches.
Decision making
Both approaches have WIP patches that are close to RTBC-able, both have pros and cons. Profiling of both the server-side and client-side impacts of each approach will hopefully clearly favour a single approach - if not, we're going to have to make some hard decisions.
User interface changes
It is possible that, depending on the final implementation, links that are not "navigation" links will never have the "active" class applied. Otherwise, there will be no UI changes.
API changes
The "opt-in" server-side approach introduces a new boolean flag for link generation functions that can be used to bypass/invoke "active" class logic.
Related Issues
- #1830598: [meta] support render caching properly
- #2102777: Allow theme_links to use routes as well as href , #2047619: Add a link generator service for route-based links - profiling shows that route-based link generation is slower than anything we've had in core previously
- #1279226: jQuery and Drupal JavaScript libraries and settings are output even when no JS is added to the page - Drupal 8 could support pages with no javascript (issue still open at time of writing)
- #1805054: Cache localized, access filtered, URL resolved, (and rendered?) menu trees
Original report by @catch
Part of #1830598: [meta] support render caching properly .
The active class added by l() means that any content rendered with an l() call in it can only be cached per page - otherwise the .active class will be wrong.
If we added that via JavaScript instead, the markup would be the same for all pages. This applies to the toolbar, menu blocks, and most entity rendering.