Problem/Motivation

JavaScript has become much more important. Every site on the web loads much, much more JS than even a few years ago.
At the same time, JS is maturing: everybody is using small JS libraries for various purposes. And then writing custom JS to use those libraries.
And of course, for all those small nice JS-powered things, we need CSS to style it.

Together, that's a lot of additional assets to load. But we have aggregation for that, so it doesn't harm us, yay!

Except that we also have drupalSettings.ajaxPageState, to track which assets have already been loaded. This allows us to not load already-loaded CSS/JS again when loading additional content via AJAX, or using Drupal 8's Dialog system to open dialogs.

Consequently, drupalSettings.ajaxPageState has grown. For a stock Drupal 8, when logged in, the front page has 4.3 KB of AJAX page state! And "rich" Drupal 7 sites like popsci.com or acquia.com have the same problem: 5 and 8 KB, respectively, and that's as the anonymous user! Sites like dev.twitter.com and whitehouse.gov simply remove drupalSettings.ajaxPageState, presumably because it's slowing them down.

This means that in the HTML <head>, we are loading several kilobytes of metadata. This means that it becomes increasingly likely that the critical content (what Drupal calls "main content") is not going to be sent first. Worse, since AJAX page state is put in the HTML before the HTML that loads JS files, it also means that we make it increasingly likely that a browser won't be able to start loading JS during the first received TCP packet.
You see, the first TCP packet can contain some ~14 KB of data (~16 KB on my localhost). If 4, 5, 8, and likely even more kilobytes of that are spent on AJAX page state… then that means that the browser is unable to kick off early requests for assets that need to be loaded. And it also means that the main content (e.g. node 5) is not going to be in that first packet either, and hence the assets therein (e.g. images) will also only start loading later.

Finally, the reason all of this truly matters, because it's quite easy to dismiss it: it feels like a micro-optimization, right? That reason is: latency. A huge part of the world is only connected to the internet via mobile networking technologies like EDGE, 3G and 4G if they're lucky. In fact, most of us are using the internet via our mobile devices, very often via 3G. I think everybody has seen extremely slow page loads on those networks.
In other words: if we want Drupal to be perceived as fast, including on mobile devices, then we have no choice but to do this.

Please see https://docs.google.com/presentation/d/1IRHyU7_crIiCjl0Gvue0WY3eY_eYvFQv... for details about optimizing the first TCP packet.

Finally, this is also a hard blocker for the following important performance issues, that have long-term consequences for Drupal 8:

  1. #1014086: Stampedes and cold cache performance issues with css/js aggregation — many a high-profile Drupal 7 site has suffered from this: massive cache stampedes bring the server to its knees. This issue paves the way for generating CSS/JS aggregates the same way we generate Image Styles, because it removes the need to know dozens and dozens of file paths in favor of only the absolutely essential library names, which can easily be conveyed in a GET request.
  2. #956186: Allow AJAX to use GET requests — without this issue, this is simply impossible to do, yet AJAX GET requests are a widely recognized best practice, because it allows AJAX responses to be cached (on the client side, in ISP proxies, in a reverse proxy). The sheer size of drupalSettings.ajaxPageState makes it impossible to have GET requests.
  3. #1305882: drupal_html_id() considered harmful; remove ajax_html_ids to use GET (not POST) AJAX requests — real-world Drupal 7 sites are seeing significant slowdowns because of this, plus it's actually broken. See the summary of that issue for more details. This is a sister issue of this issue, and another required child for the preceding issue.

Proposed resolution

  1. A big part of the costliness of AJAX page state's current implementation is the fact that it lists Drupal-root-relative JS assets and basenames of CSS assets. That's… very wordy. But, now 99% of our attached assets are libraries… which means we can track which assets are loaded by using libraries exclusively. So I propose that we change drupalSettings.ajaxPageState from containing
    {
      theme: 'bartik':
      theme_token: '<token>',
      css: {…},
      js: {…},
    }
    

    to:

    {
      theme: 'bartik':
      theme_token: '<token>',
      library: …,
    }
    
  2. Convert the remaining 1% to libraries also, and remove the ability to attach individual CSS and JS assets.

Remaining tasks

Get child issues committed:

  1. #2377397: Themes should use libraries, not individual stylesheets
  2. #2378095: Convert all remaining attached individual CSS/JS assets to attached asset libraries
  3. #2378263: hook_library_alter() must be manually invoked by users of LibraryDiscovery, and has no test coverage
  4. #2378789: Views output cache is broken
  5. #2382557: Change JS settings into a separate asset type
  6. #2382533: Attach assets only via the asset library system

At the same time:

  1. Implement a prototype.
  2. Build consensus/review

Then: commit.

Optionally, make it possible for JS to load additional asset libraries using the elegant/simple architecture used by Gmail: https://docs.google.com/presentation/d/1Q44eWLI2qvZnmCF5oD2jCw-FFql9dYg3...

User interface changes

None.

API changes

Removed already-deprecated APIs, the removal of which we've been working towards for several years, hence strictly speaking these are not API changes:

  • _drupal_add_css()
  • _drupal_add_js()
  • _drupal_add_library()

Actual API changes (which can be summarized as: introduce unit-tested, well-defined ways of resolving library dependencies and resolving libraries into concrete assets):

  • Removed: drupal_sort_css_js() — this was only used internally, and because it was legacy procedural code, strictly speaking, anybody could be calling this. In practice, this is extremely unlikely
  • Removed: drupal_get_css() — less than 1% of contrib modules use this, if even 0.5% — it's only necessary if they want to output the HTML to load CSS manually for a very special use case
  • Removed: drupal_get_js() — same as above, but for JS
  • Changed: drupal_process_attached() — instead of processing/storing the attached assets in a global (which is precisely what we've been working to get rid of for several years), this now only handles non-asset attachments.
  • Added: AttachedAssets(Interface): a value object for tracking the attached assets for a response, as well as the libraries the client has already loaded, if any. This is a value object that AssetResolver's methods accept, and it's how AssetResolver can remain stateless.
  • Added: AssetResolver(Interface): given an AttachedAssets value object, this stateless service can determine which concrete CSS and JS assets should be loaded. Instead of myriads of strange edge cases being encapsulated in _drupal_add_(css|js)() and drupal_get_(css|js)(), this now makes it abundantly clear what is happening. But, not to worry, 99% of developers won't ever to have interact with it; Drupal automatically calls it with the attached assets when rendering responses.
    The methods AssetResolver::getCssAssets() and AssetResolver::getJsAssets()now contain the logic of _drupal_add_css() + drupal_get_css() and _drupal_add_js() + drupal_get_js(), respectively.
    This new class/service reuses the test coverage in AttachedAssetsTest.
  • Added: LibraryDependencyResolver(Interface): given a list of libraries, can expand that list of libraries with their dependencies, but can also calculate the minimal representative subset. This logic used to be implemented in a very convoluted way in _drupal_add_library(), relying on statics to work.
    The minimal representative subset is what is crucial in sending as little information as possible to the client-side: e.g. if you attach the core/drupal.autocomplete, core/jquery and core/jquery.ui.position libraries, then we're going to load those two libraries plus all of their dependencies. But the dependencies of core/drupal.autocomplete are core/jquery, core/drupal, core/drupal.ajax, core/jquery.ui.autocomplete, and the dependencies of core/jquery.ui.autocomplete are core/jquery.ui, core/jquery.ui.widget, core/jquery.ui.position and core/jquery.ui.menu. We don't want to list all of those dependencies: that'd be both space consuming and unnecessary: we can just list core/drupal.autocomplete and send that to the server with each AJAX request, the server (Drupal) then knows that all dependencies have been loaded, it doesn't need us to send the full list. Also, the second library that we attached was core/jquery, but that's actually already a dependency of core/drupal.autocomplete, so we don't need to list that either. And finally, core/jquery.ui.position is *also* a dependency of core/drupal.autocomplete, although an indirect one. Nevertheless, that means we don't need to list that either.
    Therefore the minimal representative subset in this example is core/drupal.autocomplete. That's right: that single library is sufficient! In computer science/graph theory terms: each asset library has dependencies and can therefore be seen as a tree. Hence the given list of libraries represent a forest. This function returns all roots of trees that are not a subtree of another tree in the forest.
    This new class comes with comprehensive unit test coverage.
  • Added: AjaxResponse::setAttachments(), to no longer rely on the statics in _drupal_add_(css|js)() to make Ajax responses aware of CSS/JS assets attached in render arrays.
  • Removed: \Drupal\Core\Render\Element\(Scripts|Styles): these were used by drupal_get_(css|js)(), respectively, for rendering the CSS/JS asset collection into HTML. They're now obsolete. Their documentation has been retained; it's been moved to (Css|Js)CollectionRenderer.
  • Changed: drupalSettings.ajaxPageState is only added if it's actually necessary, i.e. if the core/drupal.ajax library is loaded.
  • Added: hook_(css|js|js_settings)_alter() receive an additional parameter now: \Drupal\Core\Asset\AttachedAssetsInterface $assets. This provides extra context, and might be necessary in the decision in what to do in those alter hooks. system_js_settings_alter() uses it to generate the ajaxPageState setting.
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers’s picture

Issue summary: View changes
Wim Leers’s picture

Issue summary: View changes
Wim Leers’s picture

Issue summary: View changes
catch’s picture

Priority: Major » Critical

The removal of support for individual css/js is a fairly large API change, although we implicitly say everything should be a library in 8.x so it shouldn't be used that much yet.

I don't think that's something we can do after release, and this will have a large impact on mobile performance, so at least for now I'm bumping it to critical.

This also blocks #1014086: Stampedes and cold cache performance issues with css/js aggregation although that should be possible to change during any minor release once this is in place.

dawehner’s picture

The removal of support for individual css/js is a fairly large API change, although we implicitly say everything should be a library in 8.x so it shouldn't be used that much yet.

I wonder whether we can track which css/js files are not part of libraries and just add those? This would a) allow us easily to identify
which css/js files should be converted to libraries and b) not break any BC.

mgifford’s picture

dawehner’s picture

FileSize
4.18 KB

This is just a quick idea I had on the airport.

Instead of throwing away support for css/js we should still add them,
in case someone did not used a library.

This allows us to keep BC support, but also make it easy to detect which files
needs to be updated.

Wim Leers’s picture

Wim Leers’s picture

Another child issue created that is reviewable already: #2378263: hook_library_alter() must be manually invoked by users of LibraryDiscovery, and has no test coverage.

Note that the two child issues mentioned in #8 are now green and effectively blocked on review.

Wim Leers’s picture

Issue summary: View changes

Another child issue: #2378789: Views output cache is broken. The first three are all green.

Now tracking child issues in the IS.

Wim Leers’s picture

Issue summary: View changes
davidhernandez’s picture

Wim Leers’s picture

Issue summary: View changes

3 of the 4 child issues have landed. Time to open some more :)

Opened:

  1. (not blocked, patch upcoming) #2382557: Change JS settings into a separate asset type
  2. (blocked on the aforementioned issue and the other child issue that is not yet committed, #2378095) #2382533: Attach assets only via the asset library system
nod_’s picture

For the record there is #956186: Allow AJAX to use GET requests Related to this too.

Wim Leers’s picture

Oh, thank you!

Wim Leers’s picture

Title: Optimize ajaxPageState to keep Drupal 8 sites fast on high-latency networks » [PP-2] Optimize ajaxPageState to keep Drupal 8 sites fast on high-latency networks

#2378095: Convert all remaining attached individual CSS/JS assets to attached asset libraries landed.

Now only blocked on #2382557: Change JS settings into a separate asset type, where consensus was reached but I just need to refactor it based on the consensus, and on #2382533: Attach assets only via the asset library system, which itself is blocked on that one too.

Almost there! :)

Wim Leers’s picture

Title: [PP-2] Optimize ajaxPageState to keep Drupal 8 sites fast on high-latency networks » [PP-1] Optimize ajaxPageState to keep Drupal 8 sites fast on high-latency networks
Anonymous’s picture

note that the TCP window size of 14kb should be considered against compressed content, not uncompressed.

if you're not sending zipped up content over a high latency, low bandwidth connecton, none of this matters.

just so we don't get carried away with 4kb - that is 4kb of something over 100, not 4kb of 14.

Wim Leers’s picture

All true.

Two buts:

  • surprisingly many sites don't send HTML gzipped.
  • several KB of poorly compressible data is still going to cost you.

Of course this isn't a silver bullet. But it should help, and if we don't do it now, we won't be able to do it later. Thanks for helping keep ourselves honest though :)

jcisio’s picture

If we were to be honest, then:
- Recent web servers enabled page compression by default. And most of Drupal sites send HTML gzipped, because page cache compression is enabled by default in Drupal.
- 14 or 16 KB packet size does not really have a significance for high-latency networks, because server sends multiple TCP packets before it stops to receive the ACK (e.g. client only sends ACK every second packet).
- ajaxPageState is compressed rather well, because parts of URL repeats.

But, a list of full path to assets in HTML is really ugly, and it personally keeps reminding me of .NET pages with 90% of HTML is used for JS post back!

The issue itself won't help much, but its children issues are great: a lot of DX improvement. It's like mathematics: to solve a purely theoretical problem, many improvements in other topics are made. Thanks Wim for the series.

mikeytown2’s picture

Optimizing the first KB is important thus putting the ajaxPageState in the footer seems like the thing to do. Won't #784626: Default all JS to the footer, allow asset libraries to force their JS to the header do this for us?

catch’s picture

I just checked a Drupal 7 site - copy and pasted ajaxPageState to a file, then gzipped the file.

Uncompressed:

8833 Dec  5 10:33 ajax.txt

Compressed:

1519 Dec  5 10:33 ajax.txt.gz

So even compressed that's over 10% of the 14kb.

catch’s picture

@mikeytown2 if we can put drupalSettings in the footer that'd be great, but reducing its size seems worth doing independently of that - even if we default it to the footer, there'll still be sites where it ends up at the top.

Wim Leers’s picture

Title: [PP-1] Optimize ajaxPageState to keep Drupal 8 sites fast on high-latency networks » Optimize ajaxPageState to keep Drupal 8 sites fast on high-latency networks
Assigned: Unassigned » Wim Leers

#2378095: Convert all remaining attached individual CSS/JS assets to attached asset libraries just landed, so now this issue is fully unblocked! :) Working on this patch now…

catch’s picture

catch’s picture

Would be good to document all the issues blocked on this one (imagecache asset generation, GET for AJAX requests) in the issue summary bette.

chx’s picture

https://www.npmjs.com/package/crapify this might help testing this.

Wim Leers’s picture

Title: Optimize ajaxPageState to keep Drupal 8 sites fast on high-latency networks » Optimize ajaxPageState to keep Drupal 8 sites fast on high-latency networks, prevent CSS/JS aggregation from taking down sites and use HTTP GET for AJAX requests
Issue summary: View changes

#26: done! Also tried to improve the title.

Wim Leers’s picture

Issue summary: View changes
Status: Active » Needs review
Issue tags: +DX (Developer Experience)
FileSize
155.58 KB

Finally, a patch. Diffstat:

39 files changed, 1371 insertions(+), 1204 deletions(-)

Updated the IS with the API changes.


(Note: the IS currently says Drupal 8 sends 4.3 KB of AJAX page state; that's now gone up to 4.9 KB…

Before
Frontpage, as user 1: 4876 bytes:
"js":{"core\/assets\/vendor\/html5shiv\/html5.js":1,"core\/assets\/vendor\/jquery\/jquery.min.js":1,"core\/assets\/vendor\/domready\/ready.min.js":1,"core\/misc\/drupal.js":1,"core\/assets\/vendor\/underscore\/underscore-min.js":1,"core\/assets\/vendor\/backbone\/backbone-min.js":1,"core\/assets\/vendor\/modernizr\/modernizr.min.js":1,"core\/assets\/vendor\/jquery-once\/jquery.once.js":1,"core\/modules\/contextual\/js\/contextual.js":1,"core\/modules\/contextual\/js\/models\/StateModel.js":1,"core\/modules\/contextual\/js\/views\/AuralView.js":1,"core\/modules\/contextual\/js\/views\/KeyboardView.js":1,"core\/modules\/contextual\/js\/views\/RegionView.js":1,"core\/modules\/contextual\/js\/views\/VisualView.js":1,"core\/assets\/vendor\/jquery-form\/jquery.form.js":1,"core\/assets\/vendor\/jquery.ui\/ui\/jquery.ui.position.js":1,"core\/misc\/debounce.js":1,"core\/misc\/displace.js":1,"core\/assets\/vendor\/jquery.cookie\/jquery.cookie.min.js":1,"core\/misc\/form.js":1,"core\/misc\/progress.js":1,"core\/misc\/ajax.js":1,"core\/assets\/vendor\/jquery.ui\/ui\/jquery.ui.core.js":1,"core\/assets\/vendor\/jquery.ui\/ui\/jquery.ui.widget.js":1,"core\/assets\/vendor\/jquery.ui\/ui\/jquery.ui.button.js":1,"core\/assets\/vendor\/jquery.ui\/ui\/jquery.ui.mouse.js":1,"core\/assets\/vendor\/jquery.ui\/ui\/jquery.ui.draggable.js":1,"core\/assets\/vendor\/jquery.ui\/ui\/jquery.ui.resizable.js":1,"core\/assets\/vendor\/jquery.ui\/ui\/jquery.ui.dialog.js":1,"core\/misc\/dialog\/dialog.js":1,"core\/misc\/dialog\/dialog.position.js":1,"core\/misc\/dialog\/dialog.jquery-ui.js":1,"core\/modules\/quickedit\/js\/quickedit.js":1,"core\/modules\/quickedit\/js\/util.js":1,"core\/modules\/quickedit\/js\/models\/BaseModel.js":1,"core\/modules\/quickedit\/js\/models\/AppModel.js":1,"core\/modules\/quickedit\/js\/models\/EntityModel.js":1,"core\/modules\/quickedit\/js\/models\/FieldModel.js":1,"core\/modules\/quickedit\/js\/models\/EditorModel.js":1,"core\/modules\/quickedit\/js\/views\/AppView.js":1,"core\/modules\/quickedit\/js\/views\/FieldDecorationView.js":1,"core\/modules\/quickedit\/js\/views\/EntityDecorationView.js":1,"core\/modules\/quickedit\/js\/views\/EntityToolbarView.js":1,"core\/modules\/quickedit\/js\/views\/ContextualLinkView.js":1,"core\/modules\/quickedit\/js\/views\/FieldToolbarView.js":1,"core\/modules\/quickedit\/js\/views\/EditorView.js":1,"core\/modules\/quickedit\/js\/theme.js":1,"core\/assets\/vendor\/classList\/classList.min.js":1,"core\/misc\/active-link.js":1,"core\/modules\/views\/js\/views-contextual.js":1,"core\/misc\/announce.js":1,"core\/assets\/vendor\/matchMedia\/matchMedia.min.js":1,"core\/assets\/vendor\/matchMedia\/matchMedia.addListener.min.js":1,"core\/modules\/toolbar\/js\/toolbar.menu.js":1,"core\/modules\/toolbar\/js\/toolbar.js":1,"core\/modules\/toolbar\/js\/models\/MenuModel.js":1,"core\/modules\/toolbar\/js\/models\/ToolbarModel.js":1,"core\/modules\/toolbar\/js\/views\/BodyVisualView.js":1,"core\/modules\/toolbar\/js\/views\/MenuVisualView.js":1,"core\/modules\/toolbar\/js\/views\/ToolbarAuralView.js":1,"core\/modules\/toolbar\/js\/views\/ToolbarVisualView.js":1,"core\/assets\/vendor\/jquery-joyride\/jquery.joyride-2.0.3.js":1,"core\/modules\/tour\/js\/tour.js":1,"core\/misc\/tabbingmanager.js":1,"core\/modules\/contextual\/js\/contextual.toolbar.js":1,"core\/modules\/contextual\/js\/toolbar\/models\/StateModel.js":1,"core\/modules\/contextual\/js\/toolbar\/views\/AuralView.js":1,"core\/modules\/contextual\/js\/toolbar\/views\/VisualView.js":1,"core\/modules\/toolbar\/js\/escapeAdmin.js":1},"css":{"contextual.module.css":1,"contextual.theme.css":1,"contextual.icons.css":1,"jquery.ui.core.css":1,"jquery.ui.theme.css":1,"jquery.ui.button.css":1,"jquery.ui.resizable.css":1,"jquery.ui.dialog.css":1,"dialog.theme.css":1,"quickedit.module.css":1,"quickedit.theme.css":1,"quickedit.icons.css":1,"quickedit.css":1,"normalize.css":1,"system.module.css":1,"system.theme.css":1,"layout.css":1,"elements.css":1,"typography.css":1,"admin.css":1,"book.css":1,"breadcrumb.css":1,"captions.css":1,"comments.css":1,"content.css":1,"contextual.css":1,"dropbutton.component.css":1,"featured.css":1,"footer.css":1,"form.css":1,"forum.css":1,"header.css":1,"help.css":1,"highlighted.css":1,"list.css":1,"messages.css":1,"node-preview.css":1,"pager.css":1,"primary-menu.css":1,"search.css":1,"search-results.css":1,"secondary-menu.css":1,"shortcut.css":1,"skip-link.css":1,"sidebar.css":1,"table.css":1,"tabs.css":1,"tips.css":1,"triptych.css":1,"user.css":1,"vertical-tabs.component.css":1,"views.css":1,"buttons.css":1,"media.css":1,"colors.css":1,"print.css":1,"views.module.css":1,"toolbar.menu.css":1,"toolbar.module.css":1,"toolbar.theme.css":1,"toolbar.icons.css":1,"user.icons.css":1,"tour.module.css":1,"contextual.toolbar.css":1,"shortcut.module.css":1,"shortcut.theme.css":1,"shortcut.icons.css":1}
After
Frontpage, as user 1: 356 bytes:
"libraries":"bartik\/global-styling,classy\/base,contextual\/drupal.contextual-links,contextual\/drupal.contextual-toolbar,core\/drupal.active-link,core\/html5shiv,quickedit\/quickedit,shortcut\/drupal.shortcut,system\/base,toolbar\/toolbar,toolbar\/toolbar.escapeAdmin,tour\/tour,user\/drupal.user.icons,views\/views.contextual-links,views\/views.module"

That's 4.5 KB less data to send on a stock Standard profile install on the front page. The new asset AJAX page state data is 356/4876=7.3% of the original, a 92.7% improvement. The total AJAX page state (including theme metadata) is then going to be 4971-4876+356=451 bytes, so 451/4971=9.1% of the original.
And this is on stock Drupal; the difference is going to be even starker on real sites!


This also happens to be a great way to spot problems: libraries that are unexpectedly listed there either:

  • should not be loaded at all (e.g. views/views.module, only contains table and grid style styling, neither of which is used for the frontpage view) and hence expose front-end performance problems
  • should be a dependency of another library (e.g. classy/base is implicitly a dependency of bartik/global-styling)

I.e. a net DX improvement.

Wim Leers’s picture

For those of you wondering why I'm not listing every library: that's done for a good reason. See the "minimal representative subset" code, also explained in the IS:

The minimal representative subset is what is crucial in sending as little information as possible to the client-side: e.g. if you attach the core/drupal.autocomplete, core/jquery and core/jquery.ui.position libraries, then we're going to load those two libraries plus all of their dependencies. But the dependencies of core/drupal.autocomplete are core/jquery, core/drupal, core/drupal.ajax, core/jquery.ui.autocomplete, and the dependencies of core/jquery.ui.autocomplete are core/jquery.ui, core/jquery.ui.widget, core/jquery.ui.position and core/jquery.ui.menu. We don't want to list all of those dependencies: that'd be both space consuming and unnecessary: we can just list core/drupal.autocomplete and send that to the server with each AJAX request, the server (Drupal) then knows that all dependencies have been loaded, it doesn't need us to send the full list. Also, the second library that we attached was core/jquery, but that's actually already a dependency of core/drupal.autocomplete, so we don't need to list that either. And finally, core/jquery.ui.position is *also* a dependency of core/drupal.autocomplete, although an indirect one. Nevertheless, that means we don't need to list that either.
Therefore the minimal representative subset in this example is core/drupal.autocomplete. That's right: that single library is sufficient! In computer science/graph theory terms: each asset library has dependencies and can therefore be seen as a tree. Hence the given list of libraries represent a forest. This function returns all roots of trees that are not a subtree of another tree in the forest.

Status: Needs review » Needs work

The last submitted patch, 29: optimize_ajaxpagestate-2368797-29.patch, failed testing.

dawehner’s picture

Just a relative quick review.

  1. +++ b/core/includes/common.inc
    @@ -1439,135 +952,6 @@ function drupal_js_defaults($data = NULL) {
    - */
    -function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALSE, $is_ajax = FALSE, $theme_add_js = TRUE) {
    

    Meh, we have not marked this method as deprecated :(

  2. +++ b/core/includes/common.inc
    @@ -1650,49 +1034,17 @@ function drupal_merge_attached(array $a, array $b) {
    +function drupal_process_attached(array $elements) {
    

    Given that we no longer handle library and drupalSettings we have to update the documentation on that method itself. It talks about 'library' ...

  3. +++ b/core/includes/theme.inc
    @@ -1403,11 +1403,26 @@ function template_preprocess_html(&$variables) {
    +  // Optimize CSS/JS if necessary, but only during normal site operation.
    +  $optimize_css = !defined('MAINTENANCE_MODE') && \Drupal::config('system.performance')->get('css.preprocess');
    +  $optimize_js = !defined('MAINTENANCE_MODE') && \Drupal::config('system.performance')->get('js.preprocess');
    

    Let's please explain WHY we want to do that.

    It is a bit sad that this logic lives in a preprocess function.

  4. +++ b/core/includes/theme.inc
    @@ -1403,11 +1403,26 @@ function template_preprocess_html(&$variables) {
    +  $asset_resolver = \Drupal::service('asset.resolver');
    

    Here is a question: Do we need a service with state here, or would it be enough to have a service which you can ask for a resolver with state? I don't have an overview of the patch yet, but at least here, there is just a single place where this is used.

  5. +++ b/core/includes/theme.inc
    @@ -1403,11 +1403,26 @@ function template_preprocess_html(&$variables) {
    +  $asset_resolver->setLibraries($all_attached['#attached']['library'])
    +    ->setAlreadyLoadedLibraries([]);
    

    Its odd, given that this might not be true for implementation using mechanisms like turbolinks.

  6. +++ b/core/includes/theme.inc
    @@ -1403,11 +1403,26 @@ function template_preprocess_html(&$variables) {
    +  if (isset($all_attached['#attached']['drupalSettings'])) {
    +    $asset_resolver->setSettings($all_attached['#attached']['drupalSettings']);
    +  }
    

    Do we maybe want to have a method which makes simply an #attached array?

  7. +++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php
    @@ -90,77 +116,54 @@ public function prepareResponse(Request $request) {
    -        $function = '_drupal_add_' . $type;
    -        $items[$type] = $function();
    -        \Drupal::moduleHandler()->alter($type, $items[$type]);
    

    What happened with the existing alter hook?

  8. +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php
    @@ -0,0 +1,475 @@
    +/**
    + * The default asset resolver.
    + */
    +class AssetResolver {
    +
    

    Can we please describe what this is doing?

    Oh wait, we should just implement the existing interface ...

  9. +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php
    @@ -0,0 +1,475 @@
    +  public function setAlreadyLoadedLibraries(array $libraries) {
    +    $this->alreadyLoadedLibraries = $libraries;
    +    $this->alreadyLoadedLibrariesWithDependencies = $this->libraryDependencyResolver->getLibrariesWithDependencies($this->alreadyLoadedLibraries);
    +    return $this;
    +  }
    

    Do we maybe want to append the existing ones?

  10. +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php
    @@ -0,0 +1,475 @@
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function reset() {
    +    $this->libraries = [];
    +    $this->librariesWithDependencies = [];
    +    $this->settings = [];
    +    $this->alreadyLoadedLibraries = [];
    +    $this->alreadyLoadedLibrariesWithDependencies = [];
    +    $this->jsAssets = NULL;
    +    return $this;
    +  }
    +
    +  /**
    

    Do we have a use for this method? I cannot find one in the patch. Note: If this is just a temporary object (see other comment) we would not need a reset() anyway.

  11. +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php
    @@ -0,0 +1,475 @@
    +  public function getCssAssets($optimize) {
    +    $theme_info = \Drupal::theme()->getActiveTheme();
    +
    +    $css = [];
    ...
    +    // Allow modules and themes to alter the CSS assets.
    +    \Drupal::moduleHandler()->alter('css', $css);
    +    \Drupal::theme()->alter('css', $css);
    +
    ...
    +    \Drupal::moduleHandler()->alter('js_settings', $settings);
    +    \Drupal::theme()->alter('js_settings', $settings);
    +
    

    Can we inject the dependency, or at least add a todo and a follow up?

  12. +++ b/core/tests/Drupal/Tests/Core/Asset/LibraryDependencyResolverTest.php
    @@ -0,0 +1,177 @@
    +  public function providerTestGetLibrariesWithDependencies() {
    +    return [
    

    <3

  13. +++ b/core/tests/Drupal/Tests/Core/Controller/AjaxRendererTest.php
    @@ -70,6 +70,9 @@ class TestAjaxRenderer extends AjaxRenderer {
    +    if (!isset($elements['#attached'])) {
    +      $elements['#attached'] = [];
    +    }
    

    Can't you just do that with $elements += ['#attached'];

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
176.61 KB
23.91 KB

This doesn't address #32 yet.


A few of the failures were oversights on my part. But most of them were due to an API change that I hadn't yet mentioned in the IS: as of this patch, ajaxPageState is only added if it's actually necessary, i.e. if the core/drupal.ajax library is loaded. This makes AJAX page state compliant with the overarching rule: we only attach drupalSettings if an attached library depends on it.

Most of the test failures in #29 occurred because they relied on ajaxPageState being present always, and absent in certain test case scenarios in DialogTest, ViewAjaxTest and RowUITest that are very synthetic. They were extremely painful to fix.

A few of the test failures were due to areas I'd overlooked: the OpenDialogCommand still attached the core/drupal.dialog.ajax library via drupal_process_attached(), to put it in the _drupal_add_js() static, but that of course doesn't work anymore.

We should really untangle the mess that is the ajax_test test module, because the incredible hacks upon hacks that we've gathered there over time make it an absolute hell to fix any test failures in that area. Not doing that here, because the patch is already big enough.


A few problems that were spotted in the current patch, fixed in this reroll:

  1. +++ b/core/core.services.yml
    @@ -1074,6 +1074,13 @@ services:
       library.discovery.parser:
         class: Drupal\Core\Asset\LibraryDiscoveryParser
         arguments: ['@app.root', '@module_handler']
    +    public: false
    

    Problem introduced during rebasing. Will remove that change.

  2. +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php
    @@ -0,0 +1,475 @@
    +            case 'inline':
    ...
    +            case 'inline':
    

    These switch statement cases need to be removed; they only exist because of a faulty rebase from long ago.

Wim Leers’s picture

FileSize
178.79 KB
8.02 KB
  1. I know. But the impact is extremely low, and it makes a lot of long-term sense: if we want to keep drupal_get_js(), then we have to keep the kludgy+fragile system we've been trying to get rid of around to some degree just for the very few people — if any — that are already using it in 8. This is a <1% API function.
  2. Done.
  3. First: I agree that it's sad this lives in a preprocess function. But I think the entire template_preprocess_html() function is sad; we should move that logic into a HtmlResponse class, or at least let HtmlRenderer::renderResponse() take care of this. But doing *that* is out of scope here.
    Second: these checks used to be in the #pre_render callbacks of \Drupal\Core\Render\Element\(Styles|Scripts). But #type => '(styles|scripts)' no longer makes sense to have, since A) all it did was return the output of a service, B) it was only used inside drupal_get_(css|js)().
    Did that answer it?
  4. Excellent question! I've also considered that approach, but I just wanted to get something that works done ASAP. This is exactly the sort of thing I thought/hoped would be criticized.
    I think the only thing preventing us from going with a stateless service and then sending the service the necessary state, is the advanced use case in system_js_settings_alter():
      // Now generate the values for the core/drupal.ajax library.
      // We need to send ajaxPageState settings for core/drupal.ajax if:
      // - ajaxPageState is being loaded in this Response, in which case it will
      //   already exist at $settings['ajaxPageState'] (because the core/drupal.ajax
      //   library definition specifies a placeholder 'ajaxPageState' setting).
      // - core/drupal.ajax already has been loaded and hence this is an AJAX
      //   Response in which we must send the list of extra asset libraries that are
      //   being added in this AJAX Response.
      /** @var \Drupal\Core\Asset\AssetResolverInterface $asset_resolver */
      $asset_resolver = \Drupal::service('asset.resolver');
      if (isset($settings['ajaxPageState']) || in_array('core/drupal.ajax', $asset_resolver->getAlreadyLoadedLibrariesWithDependencies())) {
        // Provide the page with information about the theme that's used, so that
        // a later AJAX request can be rendered using the same theme.
        // @see \Drupal\Core\Theme\AjaxBasePageNegotiator
        $theme_key = \Drupal::theme()->getActiveTheme()->getName();
        $settings['ajaxPageState']['theme'] = $theme_key;
        // Checks that the DB is available before filling theme_token.
        if (!defined('MAINTENANCE_MODE')) {
          $settings['ajaxPageState']['theme_token'] = \Drupal::csrfToken()->get($theme_key);
        }
    
        // Provide the page with information about the individual asset libraries
        // used, information not otherwise available when aggregation is enabled.
        $library_dependency_resolver = \Drupal::service('library.dependency_resolver');
        $minimal_libraries = $library_dependency_resolver->getMinimalRepresentativeSubset(array_unique(array_merge(
          $asset_resolver->getLibrariesWithDependencies(),
          $asset_resolver->getAlreadyLoadedLibrariesWithDependencies()
        )));
        sort($minimal_libraries);
        $settings['ajaxPageState']['libraries'] = implode(',', $minimal_libraries);
    

    There are two reasons for it to live there:

    1. Pattern: It's analogous to how we expand the core/drupalSettings library's drupalSettings, to replace it with the necessary dynamic values.
    2. Independence: to generate the AJAX page state, we MUST know what asset libraries are being loaded for this response, and which the client already has. But generating the AJAX page state from within AssetResolver itself — which is the only possible alternative if we don't want to expose the state of AssetResolver — is at least equally ugly, because it's the content of a specific asset, and the AssetResolver should only deal with the libraries-to-concrete-assets logic, not also generate the content of a specific asset.

    Yes, this means that we still have a global state. But only for the very briefest of moments: when #type => html is being rendered, until the asset alter hooks have been fired.

    That being said, I think the ideal would indeed be to make AssetResolver stateless. I think the ideal would look like this: add a ResponseWithAssets class that subclasses Symfony's Response, which must be subclassed by any response that wants to attach assets (HtmlResponse, AjaxResponse …). It'd allow libraries and settings to be set on the response object. We could then have a KernelEvents::RESPONSE event subscriber with a low priority, that'd run before the response is actually rendered, to map asset libraries to concrete assets. It'd call the stateless AssetResolver, which would accept ResponseWithAssets objects as input. And finally, with only a single API addition — also providing the Response object to hook_(css|js|js_settings)_alter(), AJAX page state could just use the data on there, call AssetResolver with it there and … voila!
    Thoughts? (It's late here so I'm probably missing something.)

  5. Can you explain that? AFAICT Turbolinks would use AJAX page state also, or something like it. If you're on page X which contains libraries A and B, and you're going to page Y using Turbolinks, and that should contain libraries B and C, then you're going to want to only load library C.
    (Unless you're talking about "unloading" libraries… which shouldn't be necessary thanks to our Drupal.behaviors abstraction.)

    EDIT: oh, wait, I see what you mean. You're saying that in case of Turbolinks, the set of already loaded libraries would not be the empty set. Easy fix: check if ajax_page_state is available, and if so, use it. Fixed.
  6. I think you meant "take" instead of "make"? :)
    If so, then I think: no. I think it's better to be explicit here. Render arrays are a vaguely defined, almost freeform data structure. AssetResolver(Interface) and LibraryDependencyResolver(Interface) work with well-defined data structures. I think it's better to provide exact input, to not pollute those interfaces/classes with vague methods just to simplify a handful of calls.
  7. You have to look at the bigger context of the 3 LoC you quoted. Basically, AjaxResponse was doing VERY VERY … unethical things in order to have _drupal_add_(css|js)() contain only those assets that hadn't been already loaded. Basically, AjaxResponse implemented part of the logic of drupal_get_(css|js)() to modify their internal state beforehand, and then it called them with the $skip_alter parameter set to TRUE. If that's not mind-bending, then what is? :P And now it's GONE!
  8. LOL, Wim--. Fixed.
  9. The idea is that AssetResolver is only called once per request. So unless I'm misunderstanding you, I don't think that makes sense.
  10. Not currently, but because it's theoretically possible to call AssetResolver multiple times, e.g. in asset library-analyzing code, I added it. If you want to remove it, I could live with that, but it seems prudent to be able to reset state.
  11. Good catch; fixed.
  12. :)
  13. Done.
dawehner’s picture

Yes, this means that we still have a global state. But only for the very briefest of moments: when #type => html is being rendered, until the asset alter hooks have been fired.

So I wonder whether we could pass along the object in the alter hook. It is certainly part of that domain already.

EDIT: oh, wait, I see what you mean. You're saying that in case of Turbolinks, the set of already loaded libraries would not be the empty set. Easy fix: check if ajax_page_state is available, and if so, use it. Fixed.

That kind of logic is really something you would eventually like to override.

I think you meant "take" instead of "make"? :)
If so, then I think: no. I think it's better to be explicit here. Render arrays are a vaguely defined, almost freeform data structure. AssetResolver(Interface) and LibraryDependencyResolver(Interface) work with well-defined data structures. I think it's better to provide exact input, to not pollute those interfaces/classes with vague methods just to simplify a handful of calls.

I was thinkging about something like this:

$asset_resolver->addAttachments($all_attached['#attached'])

but I can totally understand your point.

Not currently, but because it's theoretically possible to call AssetResolver multiple times, e.g. in asset library-analyzing code, I added it. If you want to remove it, I could live with that, but it seems prudent to be able to reset state.

It just feels like bad design if you have to do that ... note the idea of passing around the storage would solve that at the same time.

mikeytown2’s picture

In reply to #29, in terms of json output, can we do pretty print if in "development mode"; JSON_PRETTY_PRINT. Checkout advagg_json_encode() to see it in action. Thinking this will further help in identifying libraries that are unexpectedly listed.

Wim Leers’s picture

#35:

So I wonder whether we could pass along the object in the alter hook. It is certainly part of that domain already.

That's a good point. That actually means we can separate state already; the outline of "the ideal" I provided is then not a blocker. Thanks! :)

That kind of logic is really something you would eventually like to override.

Agreed, but that'd require that "ideal".

[…] note the idea of passing around the storage would solve that at the same time.

Agreed!

Trying this right now…


#36: Thanks!

Wim Leers’s picture

FileSize
187.33 KB
51.88 KB

Implemented #35.

What do you think, dawehner? I think I like it better, but I'm not fond of the name: AttachedAssets(Interface) seems like it could be better named.

Status: Needs review » Needs work

The last submitted patch, 38: optimize_ajaxpagestate-2368797-38.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
187.11 KB
25.83 KB

And once we have that, I think this is another logical incremental improvement.

Status: Needs review » Needs work

The last submitted patch, 40: optimize_ajaxpagestate-2368797-39.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
185.81 KB
2.82 KB

Removing debug leftovers…

Wim Leers’s picture

FileSize
185.81 KB
1.05 KB

PHP 5.4 sigh.

The last submitted patch, 42: optimize_ajaxpagestate-2368797-41.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 43: optimize_ajaxpagestate-2368797-43.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
185.99 KB
1.14 KB
nod_’s picture

Tested, modals, views ui, quickedit, all working fine.

I don't have much to say on the PHP side of things, I love the clean-up of common.inc and removing calls to drupal_process_attached all over the place.

+1 for me

joelpittet’s picture

I'm a big fan of the naming of the AttachedAssets (finally using Asset in the name!). I'll see if I can't try to do a deeper review of this over the weekend, but looks very nice at first glance.

dawehner’s picture

  1. +++ b/core/includes/theme.inc
    @@ -1403,11 +1404,24 @@ function template_preprocess_html(&$variables) {
    +  // Optimize CSS/JS if necessary, but only during normal site operation.
    +  $optimize_css = !defined('MAINTENANCE_MODE') && \Drupal::config('system.performance')->get('css.preprocess');
    +  $optimize_js = !defined('MAINTENANCE_MODE') && \Drupal::config('system.performance')->get('js.preprocess');
    
    +++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php
    @@ -90,77 +117,54 @@ public function prepareResponse(Request $request) {
    +    // Aggregate CSS/JS if necessary, but only during normal site operation.
    +    $config = \Drupal::config('system.performance');
    +    $optimize_css = !defined('MAINTENANCE_MODE') && $config->get('css.preprocess');
    +    $optimize_js = !defined('MAINTENANCE_MODE') && $config->get('js.preprocess');
    

    It would be still nice WHY we are doing things here ...

  2. +++ b/core/lib/Drupal/Core/Asset/AttachedAssets.php
    @@ -0,0 +1,95 @@
    +  public function getSettings() {
    +    return $this->settings;
    +  }
    +
    +  /**
    

    missing inheritdoc

  3. +++ b/core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php
    @@ -0,0 +1,100 @@
    +    foreach ($libraries_with_unresolved_dependencies as $library) {
    +      if (!in_array($library, $final_libraries)) {
    +        list($extension, $name) = explode('/', $library, 2);
    +        $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
    +        if (!empty($definition['dependencies'])) {
    +          $final_libraries = $this->doGetDependencies($definition['dependencies'], $final_libraries);
    +        }
    +        $final_libraries[] = $library;
    +      }
    +    }
    +    return $final_libraries;
    

    first question: its odd that we fetch all of the tree on every call and not store that information as part of the library discovery somehow ?

    second question: why do we do the filtering in the recursive call for yourself and not rely on a array_intersect()right from the beginning. ... so instead of asking for the dependencies of $definition['dependencies'] we filter out the dependencies which we already resolved as part of $final_libraries.

  4. +++ b/core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php
    @@ -0,0 +1,100 @@
    +    foreach ($libraries as $library) {
    +      $with_deps[$library] = $this->getLibrariesWithDependencies([$library]);
    +    }
    

    Next question: We retrieved all the dependencies, but why do we need all of them. Isn't the intersection with $libraries enough for us, given that this is the set we try to cover?

  5. +++ b/core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php
    @@ -0,0 +1,100 @@
    +    foreach ($libraries as $library) {
    +      $exists = FALSE;
    +      foreach ($with_deps as $other_library => $dependencies) {
    +        if ($library == $other_library) {
    +          continue;
    

    Mh, I'm curious whether we could improve this by starting with a more greedy approach, like http://www.cs.ucr.edu/~neal/Papers/Young08SetCover.pdf

mondrake’s picture

+++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php
@@ -26,6 +27,32 @@ class AjaxResponse extends JsonResponse {
+  public function setAttachments(array $attachments) {
+    $this->attachments = $attachments;
+    return $this;
+  }

This method just replaces the attachments with a new value. Thinking also about #2347469: Rendering forms in AjaxResponses does not attach assets automatically, we may have several Ajax commands in a response, each targeting a different portion of the form, and each having its own attached assets. So, wouldn't it make sense to have a method that incrementally merges the input instead?

Wim Leers’s picture

FileSize
185.74 KB

Straight reroll of #46. #2303761: Move views_ajax_form_wrapper() to ViewsFormBase broke this. Next: addressing the feedback.

Wim Leers’s picture

FileSize
185.77 KB
524 bytes

#47: :)

#48: :)


#49:

  1. I answered this already in #34.3. I'm not sure what you want me to do?
  2. Done.
  3. First Q: We could definitely do that. But this patch is already big. Can't we do that in a follow-up?
    Second Q: good question. I kept the same logic as _drupal_add_library() had. See more below, as part of the next point.
  4. getLibrariesWithDependencies() returns a list, not a set! From the docs:
       * @return string[]
       *   A list of libraries, in the order they should be loaded, including their
       *   dependencies.
    

    This is used for determining which libraries to actually load, and loading libraries means loading assets, and that's sensitive to order, hence: list, not set.
    When determining which things to load, we must therefore always use lists (since order matters), but when determining which things are already loaded, sets suffice (since order doesn't matter; we only need to know if a given library was loaded or not). Hence getMinimalRepresentativeSubset()'s docs say this:

       * @return string[]
       *   A representative subset of the given set of libraries.
    
  5. Thanks for the link! For others wanting to read it: most of that paper focuses on the weighted set-cover problem, but this only needs a good algorithm for the basic set-cover problem. You can skip anything related to the weighted set-cover problem; or just assume the weight is the same for all sets.
    First: the set-cover problem is NP-complete: https://en.wikipedia.org/wiki/Set_cover_problem.
    Second: that same Wikipedia page also mentions the greedy algorithm as being the best choice.
    Third: I'd argue that we can improve the algorithm performance later, if it turns out to be a problem. I think we should go with the easiest to understand code.

    The current algorithm looks like this:

    LET minimal = EmptySet()
    
    # Iterate over all libraries
    FOREACH lib IN libraries
    
      # Check if the current library is a dependency of another library
      LET exists = false
      FOREACH (other_lib, dependencies) IN other_libraries
        IF lib IN dependencies
          exists = TRUE
          BREAK
    
      # If it's not, then add it to the minimal set.
      IF NOT exists
        minimal[] = lib
    

    The greedy algorithm (at each stage, choose the set that contains the largest number of uncovered elements) would look like this:

    LET minimal = EmptySet()
    LET is_covered = false
    LET covered_dependencies = EmptySet()
    LET all_dependencies_to_cover = EmptySet()
    
    # First determine what the set of dependencies to cover is; i.e. what universe to strive for.
    FOREACH (lib, dependencies) IN libraries
      all_dependencies_to_cover =  all_dependencies_to_cover UNION dependencies
    
    # Now, find the minimal set cover.
    WHILE NOT covered_dependencies == all_dependencies_to_cover
      # Step 1: Calculate how many uncovered dependencies each library would cover.
      LET uncovered_deps_per_lib = EmptyHash()
      FOREACH (lib, dependencies) IN libraries
        uncovered_deps_per_lib[lib] = |dependencies - covered_dependencies|
    
      # Step 2: Choose the library in uncovered_deps_per_lib with the highest value.
      LET max = 0
      LET next_lib = null
      FOREACH lib IN libraries
        IF uncovered_deps_per_lib[lib] > max
          max = uncovered_deps_per_lib[lib]
          next_lib = lib
    
      # Step 3: Add the chosen library to covered_dependencies, and go to the next iteration
      minimal[] = next_lib
      covered_dependencies = covered_dependencies UNION dependencies
    

    Not only does it look more complex, it's also more difficult to grok, because it's no longer directly using properties of the concepts we're dealing with: asset libraries. Asset libraries are fundamentally trees, not sets. By looking at it from a trees angle, as the current algorithm does (and as the documentation indicates), it keeps the algorithm simpler, IMO.


#50: Great remark! I'll be very honest: that's what I originally wanted to do. But that means AjaxResponse must use drupal_merge_attached(). And since that is still a procedural function, it has a big ripple effect: many AJAX unit tests will have to start mocking that. Since this patch already is very big, I wanted to leave that for that issue.
Furthermore, and possibly an even stronger argument: I have not ever seen a single AJAX response with multiple HTML responses. So the actual value of that would be rather low. And in those rare cases, the logic building the AJAX response could just as well do the necessary merging, by calling drupal_merge_attached() itself? :)
In any case, I'd like to get this in and then consider adding that.

Wim Leers’s picture

The weird whitespace in #52 is completely the fault of d.o's filter setup being broken, see #2396165: Code filter configuration removes blank lines.

dawehner’s picture

@Wim Leers
So we need to file a couple of follow ups first?

Jelle_S’s picture

Possibly related, or maybe a follow-up:

#1573020: Use data-* for CSS and JS files list

That issue focusses on moving the js & css files in ajaxPageState to data-attributes. Once this issue is committed we might want to consider moving drupalSettings.ajaxPageState.libraries to data-attributes?

Wim Leers’s picture

#54: I could file issues for looking into algorithmic optimizations and getting rid of theme_preprocess_html(). Did you want to see other issues filed too, or?

#55: yep, sure, that's possible :) The storage mechanism doesn't matter for this issue; what matters is which actual data needs to be sent.

dawehner’s picture

@Wim Leers
#50 also sounds like a candidate. Thank you, and sorry for annoying for todos.

nod_’s picture

@Jelle_S that issue was because I wanted to do the diff of JS/CSS files to add on the frontend so we could remove some crazy stuff on the PHP side and not send a huge ajaxPageState. This issue makes the size very reasonable so there is no need to change how we do things. The patch makes it like it should have always been (patch which is RTBC for me since I don't have anything to complain about on PHP side).

I'm closing the other issue, thanks for bringing it up.

YesCT’s picture

Issue tags: -front-end performance +frontend performance

changing to use the more common tag, so the less common one can be deleted, so it does not show up in the auto complete and confuse people.

Wim Leers’s picture

FileSize
185.74 KB

Chasing HEAD, #2403269: Update to jQuery UI 1.11.2 broke this.

Wim Leers’s picture

Note the diffstat: 58 files changed, 1494 insertions(+), 1299 deletions(-) — 711 lines are removed from common.inc, bringing that down from 2709 LOC to 1998.

(All the while bringing full unit test coverage for resolving library dependencies, which has zero explicit test coverage in HEAD.)

dawehner’s picture

Note the diffstat: 58 files changed, 1494 insertions(+), 1299 deletions(-) — 711 lines are removed from common.inc, bringing that down from 2709 LOC to 1998.

... this is quite impressive, especially because we add new functionality. Nevertheless, I am not able to remotetly judge all the implications these changes have.

Wim Leers’s picture

FileSize
185.25 KB
7.24 KB

So nod_ says he can't sign off on the PHP. dawehner says the same.

Pinged Moshe for a review.

In the mean time, all I can do to push this issue forward, is to review it myself for nitpicks and fix those. And I found a whole bunch :) Now the diffstat has gotten even better: 58 files changed, 1471 insertions(+), 1299 deletions(-).

Fabianx’s picture

  1. +++ b/core/includes/theme.inc
    @@ -1393,11 +1394,24 @@ function template_preprocess_html(&$variables) {
    +  $ajax_page_state = \Drupal::request()->request->get('ajax_page_state');
    +  $assets->setAlreadyLoadedLibraries(isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []);
    

    nit - I would love (a followup) helper function for that.

    Given this will be cleaned up anyway and removed from preprocess_html ...

  2. +++ b/core/lib/Drupal/Core/Asset/AttachedAssets.php
    @@ -0,0 +1,98 @@
    +    if (!isset($render_array['#attached'])) {
    +      throw new \LogicException('The render array has not yet been rendered, hence not all attachments have been collected yet.');
    +    }
    

    nit - Maybe also check for #printed?

  3. +++ b/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php
    @@ -84,9 +85,7 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
       protected function drupalRenderRoot(&$elements) {
    -    $output = drupal_render_root($elements);
    -    drupal_process_attached($elements);
    -    return $output;
    +    return drupal_render_root($elements);
       }
    

    Follow-up, but this should have the renderer injected or not?

Here is the todo:

Change Record:

1. setAttachments for responses (very important for Module Devs)
2. Changes to hooks like hook_js*_alter

IS:

1. hook_js*_alter API changes

----

My biggest concern is the DX around explicitly setting assets, but I feel that can be follow-up.

While explicit is good, there is more to that and I probably would have preferred some helper function to take a render array and turn it into a response (where assets and other meta data like cache tags are added / can be added directly).

That would give contrib the most flexibility, while at the moment it trains programmers to just add assets, which will make things difficult to extend for the future.

e.g.

$response = Response::fromRenderArray($build);

similar to how getCacheableRenderArray works right now.

It could still use ->setAssets internally, so explicitness is still there and no changes needed, but it also allows contrib to take other render array properties into account without having to exchange all services and potentially all contrib services, too ...

----

Besides that from my POV this is **RTBC**.

I am leaving it some days for global sprint weekend for others to review as its rather huge - unless Wim wants core committers to take a look already now ...

Wim Leers’s picture

Issue summary: View changes

Updated the IS. Next: creatinga CR.

Wim Leers’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

CR created; minor changes in IS, to improve clarity.

moshe weitzman’s picture

Status: Needs review » Reviewed & tested by the community

#47 did manual testing of this change, so I did yet another code review. The code is clear, and much improved. Test coverage is much improved. I join FabianX (#48) in calling this RTBC. Looking forward to some Performance issues being unblocked when this goes in.

moshe weitzman’s picture

Issue tags: +SprintWeekend2015
webchick’s picture

Assigned: Wim Leers » catch

Fantastic!

Since this is performance-related, tossing over to catch.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 64: optimize_ajaxpagestate-2368797-64.patch, failed testing.

rteijeiro’s picture

Status: Needs work » Needs review
FileSize
181.73 KB

Simple re-roll. Only conflicts in user.module file.

mondrake’s picture

+++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php
@@ -90,77 +117,54 @@ public function prepareResponse(Request $request) {
+      ->setAlreadyLoadedLibraries(isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : [])

Minor, I did not add core/drupal.ajax in the dependencies of my module libraries.yml file, and $ajax_page_state['libraries'] is undefined at that point, yielding to notices:

Notice: Undefined index: libraries in Drupal\Core\Ajax\AjaxResponse->ajaxRender() (line 130 of /[...]/core/lib/Drupal/Core/Ajax/AjaxResponse.php).

Notice: Undefined offset: 1 in Drupal\Core\Asset\LibraryDependencyResolver->doGetDependencies() (line 58 of /[...]/core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php).

rteijeiro’s picture

Sorry @mondrake, could you tell me what module do you mean, please?

I'm trying to find the place to add the dependency but not sure where.

mondrake’s picture

@rteijeiro sorry, of course, it's a contrib module, not core :)

I think the point here is simply that AjaxResponse::ajaxRender() is assuming that the $ajax_page_state array retrieved from the request will always have a 'libraries' key, which is not necessarily true.

Maybe just

-      ->setAlreadyLoadedLibraries(isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : [])
+      ->setAlreadyLoadedLibraries((isset($ajax_page_state) && isset($ajax_page_state['libraries'])) ? explode(',', $ajax_page_state['libraries']) : [])

would do?

BTW - adding core/drupal.ajax in my *contrib* module libraries.yml dependencies fixed the problem for me

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community
FileSize
185.27 KB

Straight reroll of the RTBC'd #64, and hence putting back at RTBC.


#73/#75: AFAICT what you're describing is impossible. I think you're testing with an improperly patched site.
Either the ajax_page_state value is present in the HTTP POST data, or it's not. If it's present, then it contains a 'libraries' key. Your problem description says that the ajax_page_state value is present, but it doesn't contain a 'libraries' key. This is impossible.
Therefore the existing check is sufficient. If it turns out I'm wrong, then we can very easily fix it in a follow-up issue.

mondrake’s picture

#76 agree, it's something wrong with what I was doing in the contrib module. Sorry for derailing and thanks for feedback.

catch’s picture

Overall this looks great. Could only really find minor comment nitpicks.

  1. +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php
    @@ -0,0 +1,347 @@
    +   * Given lists of needed & loaded libraries, resolves which libraries to load.
    

    Doesn't this just get a list of needed libraries?

  2. +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php
    @@ -0,0 +1,347 @@
    +   */
    

    It's not 100% clear whether the dependencies of the needed libraries will also be excluded from the comment. Could this say something like "A list of libraries and their dependencies in the order they should be loaded, excluding any libraries that have already been loaded"?

  3. +++ b/core/lib/Drupal/Core/Asset/AssetResolverInterface.php
    @@ -0,0 +1,78 @@
    +   * It loads the CSS in order, with 'module' first, then 'theme' afterwards.
    

    Is this 'module' first or 'default' first? Would probably just use the constants here.

  4. +++ b/core/modules/ckeditor/src/Tests/CKEditorLoadingTest.php
    @@ -157,8 +155,10 @@ protected function getThingsToCheck() {
    +      // editor/drupal.editor, hence presence of the former immplies presence of
    

    immplies

Wim Leers’s picture

FileSize
185.44 KB
4.63 KB
  1. Yes, clarified. Also fixed the doc example just below it.
  2. You're right, that @return doc was confusing, clarified.
  3. This is copied verbatim from drupal_get_css(): since I kept the same logic, I also kept the comments. But, per your request, updated it.
  4. Fixed.

Also made one tiny change in theme.inc, at chx' request: retrieve a service once, assign it to a variable, instead of retrieving the same service twice.

Since this reroll only changed comments for all intents and purposes, leaving at RTBC.

Jelle_S’s picture

Quick scan through the patch, found some more (really small) nitpicks to be added to those of #78

Ugh, sorry Wim, seems my comment is just a bit too late, I was hoping to finish the review before you addressed #78

  1. +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php
    @@ -0,0 +1,347 @@
    +    // get the JavaScript settings assets. and converts them into a single
    

    Nit: Punctuation & grammar:
    "[...] settings assets. and converts [...]" (either capital or comma)
    "[...] get the Javascript [...] converts them [...]" (get vs gets or convert vs converts)

  2. +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php
    @@ -0,0 +1,347 @@
    +   * This sort order helps optimize front-end performance while providing modules
    

    Nit: 81 chars (80 char comment limit)

  3. +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php
    @@ -0,0 +1,347 @@
    +   *   First item for comparison. The compared items should be associative arrays
    

    Nit: 81 chars (80 char comment limit)

  4. +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php
    @@ -0,0 +1,347 @@
    +    // First order by group, so that all items in the CSS_AGGREGATE_DEFAULT group
    

    Nit: 81 chars (80 char comment limit)

  5. +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php
    @@ -0,0 +1,347 @@
    +    // common files needed throughout the website. Separating this way allows for
    

    Nit: 81 chars (80 char comment limit)

  6. +++ b/core/lib/Drupal/Core/Asset/AttachedAssetsInterface.php
    @@ -0,0 +1,85 @@
    +   *   A render array
    

    Nit: Punctuation.

  7. +++ b/core/modules/system/src/Tests/Common/AttachedAssetsTest.php
    @@ -199,21 +211,23 @@ function testHeaderSetting() {
    +   * Tests that for assets that have cache = FALSE, Drupal automatically sets preprocess = FALSE.
    

    Nit: 80 char comment limit

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 79: optimize_ajaxpagestate-2368797-79.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
185.42 KB
4.34 KB

#80: np, thanks :) All addressed in this reroll! Still keeping at RTBC, because again only comment changes.


I cannot reproduce the failure in #79 locally, assuming it was a random fail for now.

catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.0.x, thanks! Lot of work to get here.

  • catch committed dd3a597 on 8.0.x
    Issue #2368797 by Wim Leers, dawehner, rteijeiro: Optimize ajaxPageState...
Wim Leers’s picture

YAY! Thanks for the commit! Next: those requested follow-ups, all of which will be non-critical :)

Wim Leers’s picture

#36: actually, JSON_UNESCAPED_SLASHES is what we want :) But, turns out this option is not turned on for a good reason: http://stackoverflow.com/questions/1580647/json-why-are-forward-slashes-.... JSON_PRETTY_PRINT while developing would be nice though, but is completely unrelated to this issue. Feel free to file a feature request against 8.1, I'd like to have that too :)


#59:


#65:

  1. That'll be handled in #2407187: Optimize LibraryDependencyResolver::getMinimalRepresentativeSubset() and win >=4%.
  2. All this cares about is whether #attached is available or not. There's no need to check for printing.
  3. Agreed, did it as part of #2407201, see #2407201-5: Consider introducing AjaxResponse::addAttachments().

Patch that defaults JS to the footer: now available, and very simple, at #784626-148: Default all JS to the footer, allow asset libraries to force their JS to the header.


WHEW! That was all the follow-up stuff.

mondrake’s picture

ianthomas_uk’s picture

It looks like this has broken aggregation

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

xjm’s picture