Problem/Motivation
There are several previous issues discussing this, but all several years old and mostly abandoned because we didn't have either a JavaScript build step or #1014086: Stampedes and cold cache performance issues with css/js aggregation in core. Should be added as related issues.
Postponed on #2258313: Add license information to aggregated assets which we need to fulfil GPL licensing requirements.
CSS minification has very different requirements and is discussed in #3302612: Consider adopting a third party CSS minification library.
There are broadly two approaches we could take:
1. Adopt a PHP JavaScript minifier/uglifier, and then use this to process aggregates. The disadvantages are more CPU/time building aggregates, and PHP not being the best language to process JavaScript.
2. Uglify/minify JavaScript assets via the build step to .min.js files (which get committed to the git repo), and then use those when building aggregates.
On-the-fly minification with JavaScript isn't doable without introducing a node dependency to core. Having some minification in core is a lot better than none, even if better theoretical methods exist, because only a small fraction of sites will install contrib modules (let alone server-side dependencies) purely to improve front-end performance.
Since we can ensure that all core JavaScript is minified via a build step, but can't do this for contributed modules, we can do both:
1. Minify core JavaScript to foo.js - the original will be at foo.es6.js or foo.source.js, and add {minified: true} to the library definition.
2. Add on-the-fly minification with Peast to any JavaScript that's not pre-minified.
Steps to reproduce
Proposed resolution
Add on-the-fly minification (but not uglification) to core, using Peast https://github.com/mck89/peast
This reduces ajax.es6.js from 59kb down to around 17kb in around 100ms. There is some performance cost to this, but we create aggregates in their own dedicated request and write them to disk, so any performance penalty only hits the request that actually generates an aggregate the first time, and even then it will take less time to transfer over the network post-minification. Additionally since core files will already be minified, the only additional time spent is for unminified contrib/custom JavaScript.
Dependency evaluation
- Maintained by Marco Marchio (https://github.com/mck89) with approximately nine other contributors. First stable release was in 2017
- Does not have a formal security policy. (request made in https://github.com/mck89/peast/issues/51)
- Release cycle: Patch releases every 2-4 months, minor releases every 1-2 years. Last release was May 2022.
- Follows the ECMAScript specification, so a proper AST JavaScript parser rather than simply a minifier. Includes general documentation beyond the README at https://github.com/mck89/peast/tree/master/doc. Decent looking test suite: https://github.com/mck89/peast/tree/master/test/Peast
- No other dependencies.
Remaining tasks
None.
User interface changes
None.
API changes
None.
Data model changes
None.
Release notes snippet
Drupal core's JavaScript aggregation removes comments and whitespace from JavaScript files resulting in a significant filesize reduction. Sites using contribution minification solutions may wish to re-evaluate whether they can rely on core aggregation instead.
| Comment | File | Size | Author |
|---|---|---|---|
| #94 | 3302755-94.patch | 7.84 KB | catch |
| #86 | 3302755-86.patch | 7.99 KB | catch |
| #86 | 3302755-interdiff.txt | 4.9 KB | catch |
| #82 | 3302755-82.patch | 9 KB | catch |
| #82 | interdiff-3302755-82.txt | 482 bytes | catch |
Comments
Comment #2
catchComment #3
catchComment #4
nod_I think merely removing comments and extra spaces is going to be enough for a core solution. Changing variable names or using shorter constructs doesn't help as much as comment and spaces removal. A couple of examples tweaking some babel config (compact meanst no comments and no whitespace):
Which means that using something like peast for parsing the JS and outputting it in the "compact" format will be sufficient. A few benchmarks would be necessary to see what's the fastest option as well.
It's plugable so contrib can provide a better solution if necessary. I'm thinking this is a good enough solution for most people.
Comment #5
catchThe numbers in #4 are great! If we can get something like that using peast agreed that'll be good enough for most people.
Comment #6
nod_This is what we do with CSS aggregates so JS might be a bit more tricky to handle but the principle is the same, we'll need to have a look at the actual cost of compacting JS vs. CSS.
Comment #7
catchNow that it's not a blocking operation for HTML page requests, and that aggregates can be requested in parallel instead of created strictly in sequence, we've quite a bit of headroom, and those savings are a lot.
Comment #8
nod_was curious about jquery ui files, here is a few samples (using terser as a minifier, so not a direct comparison with the above table)
Same table as in #4 with terser doing the minification, not babel:
Comment #9
catchThis is postponed on #2258313: Add license information to aggregated assets but only from the point of view of GPL licensing, so I think it's worth a viable patch here regardless to see where we end up.
Used Peast per nod_'s suggestion above, we'll need to do a dependency evaluation, but it has everything we need and is easy to use.
I'm attaching a patch that just adds the minification (no line breaks, comments, or optional braces).
I'm also attaching a patch that I used for timing - via hrtime() as well as the output. Processor on my local machine is an 8th generation i5, nothing special. I tested the front page of a 10.1.x standard install logged in a user/1 - this has quite a lot of CSS and JavaScript on it (contextual, toolbar, bigpipe etc.)
Small files like touchevents-test.js take ~1-2ms to process each.
drupal.js and contextual.js take ~18ms each.
ajax.js is the worst at ~97ms.
This is roughly linear with the size of the original files - but note below, we already strip nearly all comments from these files via the js build step
Next: a comparison of aggregate file sizes.
Before:
After:
On first glance, 294k down to 266k doesn't look that great, but this is deceptive because the aggregate has jQuery and Underscore in it, both of which are pre-minified so not touched by this patch.
jquery.min.js is 88k, underscore is 20k - this is a non-exhaustive list of the pre-minified JavaScript in the aggregate, there's also tabbable, shepherd and others. I think nod_'s numbers above are probably better for actual comparisons.
Also, our own files already have comments stripped by the js build step.
ajax.js is 22k, ajax.es6.js is 59k
Running the same Peast process against ajax.js or ajax.es6.js reduces both to 17k (i.e. the end result should be identical despite very different savings).
Timing Peast compression of ajax.js vs. ajax.es6.js is within 10% of each other, likely because ignoring comments is cheap compared to parsing and formatting the actual code.
This means the filesize savings will be more dramatic after #3278415: Remove usages of the JavaScript ES6 build step, the build step itself, and associated dev dependencies without much extra CPU time.
Finally, browser timings from chromium for loading aggregates (and modernizr) - this is on localhost, so the least-friendly test given a 'normal' request would involve network latency.
This shows about 114ms for aggregate generation with no minification, vs about 400ms for aggregate generation with it - i.e. approximately 300ms additional time - more or less matches the raw timings of 100ms + 20ms + 20ms + lots of 1-10ms from the PHP timing.
For the tiny aggregate with hardly anything in it, it took less time to serve which shows the minification is within margin of error when there's not much to minify - the 1 or 2ms that takes is dwarfed by even local network time.
So if we assume the build step is removed, we're looking at hundreds of kb of file size reduction, vs. 200-400ms extra time for asset generation in a relatively bad case of a full admin.
But also we've taken the asset generation out of the page request in #1014086: Stampedes and cold cache performance issues with css/js aggregation - so given CSS + JavaScript aggregates could be 50-150ms each to generate, and we might have five to ten of them, that's 200 to 800ms off the HTML page and time to first byte. That means even if we add some time for minification, we're still overall spending less linear time aggregating on cold starts than Drupal 6 to Drupal 9 was.
And then the aggregates are requested in parallel, so even if the JavaScript aggregate request takes 400ms instead of 115ms, the browser is going to be loading and parsing various CSS aggregates while that's happening. I think this is more than reasonable for the completely cold start case, and every request for an aggregate afterwards will get the smaller file size.
Comment #10
catchComment #11
catchComment #12
catchComment #13
catchComment #14
nod_Less precise data, On the admin/content page: I'm going from a mobile lighthouse score of 95 (raw) to 97 (minified) with this patch (on an empty css/js folder). With cached files the scores are 96 (raw) and 98 (minified).
All in all it seems to me we're good to go ahead with this performance wise, even with the extra delay, thanks to the parallelization and size reduction it's still a performance improvement for the end-user.
Going with peast instead of another minifier will be useful when we'll want to handle JS modules in core, and automatically build library dependencies based on what is imported in the actual scripts. Since we'll have a tool that can parse JS files reliably and provide an AST we can query for informations and avoid having people declare their import in the script and in the drupal space.
Comment #15
xjmJust fixing the dictionary so tests will run.
Comment #16
catchAdding #12 and #15 together.
Comment #17
nod_I'll move to unpostpone this, the license issue appear to be much simpler than initially expected.
Good news that peast does the job. We might want to try jsminifier and a few other existing php minification libs out there to compare processing times, just to make sure peast it not overly slow compared to other libs.
Comment #18
wim leersHoly 🐄 that is very little code! 🤯 Can't believe this is finally happening! 🥳
How is this priority? 🙃 Bumped to .
We still need a dependency evaluation added to the issue summary. This also warrants a change record. And obviously this would be a release highlight!
There are 3 failures in
Drupal\Tests\Core\Asset\JsOptimizerUnitTest::testOptimize()to still address … but they are failing for poor reasons: irrelevant whitespace was stripped.Comment #19
wim leersComment #20
catchPer @nod_'s suggestion I thought I'd compare Peast and JsMinify, couldn't find anything called that, but found https://github.com/matthiasmullie/minify and tried that in case that was what he meant and also why not ;)
This is a bit different from Peast:
1. You can minify multiple files at a time if you want as well as telling them where to write to, including a gzip option. Our aggregation architecture currently doesn't allow for this, but it'd be an option for the future. One file at a time is fine too though.
2. It does CSS as well as JS.
3. It does some code shortening for both CSS and JS.
Tried it on ajax.es6.js since that's one of core's larger files, the results are actually a fair bit different:
That's more than ten times as fast - presumably because it's mostly running multiple regexps vs. converting to/from AST. This would make a noticeable difference to aggregate generation requests - say we have to minify 20 files and Peast takes an average of 20ms each, that's 400ms vs. more like 40ms for matthiasmulli/minify. Could also mean it's more prone to hard-to-fix bugs: https://github.com/matthiasmullie/minify/issues has a fair number of open reports.
Peast however results in a 200b smaller filesize, not much difference but could add up a few kb across all files in all aggregates.
Even though Peast isn't explicitly doing code shortening, it's possible the conversion to and from AST does some implicitly compared to core js code style, so that might be why.
Checked out packagist download stats:
Given the download stats depend on things like CI jobs etc., not really much in it - they've both got multiple contributors, dozens of forks, recent stable releases etc.
Uploading the (very basic, just run
drush scr ./minify-timing.php) script I used to produce the files and do the timing. Need to install both libraries via composer first.Comment #21
catch@nod_ suggested trying JShrink in Slack, here's the results:
https://github.com/tedious/JShrink
Still nearly ten times as fast as Peast, slightly higher filesize again, but I think this one might be the right trade-off: explicitly mentions minification on the fly, has a security policy, very few open bug reports etc.
10ms to cut 50kb off a file is probably enough to improve performance even for the initial cache hit that generates the files just reducing that initial transfer.
Comment #22
wim leersI think this means we need to re-assess why we'd want to use
mck89/peastversus one of the two others. Is there a technical reason to do so?Clearly performance-wise the two alternatives have a significant edge! 😮
Comment #23
catchSwitching to JShrink and fixing the test failures.
Comment #24
catch@Wim Leers looking at the bug reports against
matthiasmullie/minifyit looks like at least theoretically it might end up introducing errors where Peast wouldn't.However, the issue queue of JShrink is a fair bit healthier, it has regular releases, seems well supported etc., and it's still ten time faster than Peast, so I don't think there's a good reason to choose Peast instead. The only reason would be if we also wanted to use Peast's AST parsing for some other purpose to only have one dependency, but we're not trying to do that yet.
Comment #25
catchuhhhhh
Might be blocked on #3278415: Remove usages of the JavaScript ES6 build step, the build step itself, and associated dev dependencies unless we workaround that.
Comment #26
nod_I think those ones should be removed from the validation, they're excluded from eslint.
I think because we never updated those files after we added the commit checks we didn't see that before (last update in 2015...)
Comment #27
longwaveWow! This is looking very promising and the one-line code change is almost unbelievable.
This needs to go in core/composer.json as it's a runtime dependency of core itself.
Comment #28
longwaveHaving said that I'm not sure we can ship with JShrink with issues like these around regular expressions:
https://github.com/tedious/JShrink/issues/94
https://github.com/tedious/JShrink/issues/95
These could cause bugs in clientside JS that would be very hard to track down.
I disagree with this. While there has been a PHP 8 compatibility release, the minifier code itself in JShrink hasn't been touched for three years now.
Comment #29
mfbI'm getting strong "now they have two problems" vibes here, as far as trying to use regex to minify JS.
Peast certainly sounds like it's on a more solid technical foundation - I am assuming that front-end assets are being built pretty rarely, so correctness is more important than speed. Is there any reason not to use JS tooling to build front-end assets? That way, a known-good parser could be used to transform JS, and source maps could be built too for easier debugging. Core could distribute the source JS, minified JS and source maps.
Comment #30
catch@longwave thanks for finding those two issues against JShrink, if there was the infrequent code changes without those issues that'd be a sign it's just working great, but instead it puts it in the same bucket as matthiasmulli/minify then, so reckon we're back to Peast.
@mfb
@mfb this was discussed a lot in this 15 year old issue: #119441: Compress JS aggregation where we ended just giving up. Things have moved on a bit since then, but not really that much in the scheme of things except that a lot of the groundwork for both approaches has been done in the meantime.
There are trade-offs in both directions:
1. We could repurpose the build step that's just about to be removed in #3278415: Remove usages of the JavaScript ES6 build step, the build step itself, and associated dev dependencies, to fully minify the foo.js files and keep the foo.es6.js files in place (or rename them to foo.source.js or something). We can't do foo.js/foo.min.js without updating all the library definitions (and breaking library alters etc.).
- minification done by js (very good)
- no performance implications from minification on the fly (good)
- keep the build step (probably fine?)
- no change for js from custom or contrib modules unless they also run the build step and switch to the same file structure. This means more work for contrib and less actual front-end performance impact for real sites unless it's done. (not great)
2. This issue: use JShrink or Peast to minify on the fly.
- minification in PHP (historically bad, but if Peast is still viable then fine)
- performance impact from minification on the fly (manageable even with Peast, but it's definitely there)
- no build step (about to remove it anyway)
- js from custom and contrib modules also gets minified (very good)
The last reason for doing minification on the fly would be if we were to adopt a much more aggressive minification that shortens variable names etc. - since if multiple files are using the same variable name, they could all use the same shortened version, saving even more space. However, the benefits of doing that are very small compared to just removing whitespace and none of the current approaches even begin to try to do that, so I think we can just ignore that for now and assume whatever we do it will be file by file.
So it really comes down to whether contrib/custom js gets minification 'for free' because core's doing it on the fly, vs. using js to minify core's js and hoping contrib follows suit.
The big difference between now and ten+ years ago is that all of the groundwork (apart from licensing which is close) is in place. It's only a few minutes to move the current patch back to Peast so I'll do that to get something working. I think it's probably viable to do minification in the build step (instead of removing the build step altogether), but mostly concerned that we do one of them instead of it taking another 15 years.
Comment #31
catchBack to Peast. Need to re-do the issue summary update for the dependency evaluation.
Comment #32
nod_@mfb the issue is that we want to avoid requiring nodejs for running Drupal in production. Also the dependencies for Js tools are massive and that would become a security concern pretty fast.
For source maps see #3227125: Produce a sourcemap for aggregated JS doable in PHP fairly easily
Comment #33
catch@nod_ I think mfb's suggestion is to do minification in the build step, not on-the-fly something like this:
- Instead of shipping with
ajax.jsandajax.es6.js, we'd ship withajax.js(minified), andajax.source.js(ajax.es6.js renamed).- library definitions would continue to point to ajax.js, since that's minified, and add {minified: true} to the library definition.
To get minification of contrib or custom code, they'd need to do the same file structure/library definition update.
So this would mean retaining, but repurposing the build step, and the js dev dependencies (+ new ones), but not requiring node.js on production since everything's shipped pre-minified.
It is obviously way more efficient for sites if they literally never have to minify anything, so it's a question of implementing it and maintaining the build step over time vs. the Peast approach.
Comment #34
wim leersThis is the reason we need it: we cannot expect contrib/custom to do this.
IOW: if a site wants guarantees about minified JS (without having to have patches for most contrib modules), we need to do this issue.
Right? I'd love to be told I'm wrong about this, because I 100% agree with @catch's statement:
Comment #35
longwaveIf we want to improve performance we could take both approaches? Allow shipping minified JS, and don't try to re-minify that - we already ship many vendored
*.min.jsfiles. Provide tooling to minify JS at build time, use that for core libraries, and allow contrib/custom to do that if they wish. But if contrib/custom can't use that tooling, then we let Peast do the work.Comment #36
wim leers#35 triggers a strong Why did I not think of that? feeling 😄👍
Comment #37
catchYes #35 is great, let's do that!
I've made a start on the dependency evaluation for Peast and tried to reflect the past couple of comments in the issue summary.
Comment #38
nod_Comment #39
nod_Posted some questions in the peast queue about security: https://github.com/mck89/peast/issues/51
Comment #40
wim leersWhat does the typical real-world performance impact look like with our new approach? 🤓
Comment #41
nod_we have some data in #9 and #14 but I don't have access to a "real" site to test this on.
Also anyone using tracking/googletag manager/ads will not see any change from this patch, since they already spend several seconds on mobile just executing tracking/ads scripts.
Comment #42
wim leers🔥🤣👏
I was looking for a re-do of the data in #9 where @catch wrote
i.e. is that better in the current approach than in the approach that was tested in #9? That's what @longwave's #35 proposal that is now implemented should do: it should have better performance, but A) does it actually have that, B) how much better?
Comment #43
catch@Wim Leers #38 is functionally the same patch as was tested in #9 so the numbers are still valid if we only did this issue.
To be able to redo those timings with the 'new' #35 approach, we'd first need to repurpose the JavaScript build step to minify JavaScript files, and also add minified: true to the respective assets in library definitions. We don't have an issue for that yet, should probably be its own issue separate from this one since the change will be completely different.
Once that's done, we could compare the timings to #9, but it'd also be exactly the same as in HEAD because no minification would be happening and the code added here would be nearly a no-op, there'd just be smaller pre-minified assets to serve.
To test the hybrid approach (pre-minification for core, Peast minification for everything else), we'd need a custom-js-heavy contrib module too. And the asset generation would be whatever it is now + however many miniseconds for Peast to minify the files from that contrib module.
Comment #44
wim leersRight — I didn't think through the mechanics of doing what I asked/described in #42 🙈
But … isn't that that data I described in #42 what a core committer like yourself would like to see before committing this? Or is there a reason here that you'd be comfortable committing this without such data?
EDIT: also, it was me who added the
minifiedflag in #2307717: Inform Drupal in *.libraries.yml via a new per-JS file "minified" flag whether a JS file has been minified many years ago, and madeJsCollectionOptimizerrespect it, so what @longwave proposed has actually already been the case for a long time 😅Comment #45
wim leersAnd while I was writing that, #2258313: Add license information to aggregated assets landed, which means this is unblocked! 🚀
Comment #46
catch@Wim well the question is really whether this issue is postponed on the pre-minification of core files or not. I've just opened #3308122: Pre-minify core JavaScript so that we can start working on that.
Without that issue, the numbers for core will be the same as #9.
With that issue, the numbers for core will be significantly better than #9, varying on just exactly how many js-heavy contrib modules with unminified JavaScript are in use.
A big motivator behind #1014086: Stampedes and cold cache performance issues with css/js aggregation (apart from the improvements to stability) was moving aggregate generation out of the critical path of generating HTML requests, so that we could then do expensive things like minification. So if we hadn't come up with @longwave's hybrid approach in #35 I probably would still have argued to commit the Peast version of this despite the performance 'regression', because when combined with other changes (and real life front end performance as soon as the aggregates are generated) it would still be a performance improvement overall.
Comment #47
catchRe-titling so it's easier to tell the difference between this and #3308122: Pre-minify core JavaScript.
Comment #48
nod_We got a reply from peast maintainer about the security questions. https://github.com/mck89/peast/issues/51#issuecomment-1239084168 Essentially this is a hobby project from a single maintainer, and that he'd be happy to share maintainership since there is new syntax to implement each year when ecma releases a new version and he can't guarantee how long he'll be able to maintain the library.
Comment #49
catchObviously would be great if he's said 'definitely going to be maintained at least the next decade' but also that's a very open and honest answer and he seems happy to accept help.
New patch - needed a re-roll for composer.lock changes, and adding caching of the Peast step.
Doesn't include pre-warming yet.
Comment #50
nod_cache warming could be a follow up no?
Comment #51
catchYeah I personally think a follow-up would be fine for pre-warming. The caching in the current patch means that aggregates for anon vs. auth vs. front page vs. forms vs. admin pages won't be minifying the same files more than once already.
Fixing the typos (should've been in the last patch but I uploaded the wrong one :/).
Comment #52
nod_catch created the change record already
Added follow-up for cache warming here #3308398: [PP-1] JS minification cache warming
Assuming this is green RTBC from me only code I touched was to make the commit check pass.
Comment #53
wim leers🐛 Incomplete docblock. And does not yet create
$this->cache. Andprotected $cacheis missing from the class.🤔 Given this logic, shouldn't we remove
\Drupal\Core\Asset\JsOptimizer::clean()'s current logic too?It currently does this:
Why this change? 🤔
Comment #54
nod_#53.3: because https://www.drupal.org/pift-ci-job/2459955, those files were created without the .es6.js corresponding files because they were created before that, and before the commit checks were in place, and were not touched since. This is the first time they go through the commit checks and should be excluded since there is no reason for them to have an .es6.js version.
Thought about the clean, but if someone overrides the optimize process, there is no guarentee they'll remove those piece of comment? I guess we could remove them but it doesn't hurt to keep?
Comment #55
longwaveRe #53.1 note the
protectedkeyword in the parameter list; this is PHP 8 constructor property promotion in action which automatically creates a class property and sets it.Having said that, the parameter needs BC handling I guess.
Comment #56
longwaveThis needs to go in core/composer.json as it's a runtime dependency.
Comment #57
catch#53.1: the param docs are indeed missing, but the rest is #3278431: [May 2026] Use PHP 8 constructor property promotion for existing code! Another example in #1014086: Stampedes and cold cache performance issues with css/js aggregation. Trying to do it for new code, existing code is waiting until post-beta or later. Also @longwave's right we should do basic bc handling for the parameter.
#53.2: hmm given that's in comments and they're being stripped, we probably can! But nod_'s right this is a bit of a behaviour change so maybe in a follow-up so it gets its own CR etc?
#53.3: see #3302755-26: On-the-fly JavaScript minification.
Comment #58
wim leers#55 & #57.1 🤩 OMG YES
RM -F BOILERPLATE#57.2: WFM!
Only keeping at for #56 now :) Can't wait to see this land!
Comment #59
catchThis hopefully does it for the constructor bc. Still adjusting to the combination of property promotion + bc.
Comment #60
longwaveFWIW after using this in real world projects for a while I am kind of in favour of using explicit multiline syntax here, e.g.
as it makes it clearer to me that property promotion is being used - especially if we are to drop the @param section of the docblock. I guess this is worth discussing in the policy issue as a standard.
Comment #61
wim leers#60: that'd make it easier to modify the constructor in the future for sure: less disruptive. But yeah, it's really a detail.
Comment #62
catchI was going to say I'm not a huge fan of multi-line parameter declarations because I always want to add the trailing comma and PHP doesn't allow it. But PHP 8.0 does allow it!
Comment #63
catchInterdiff is right, patch was wrong :(
Comment #64
wim leersBeautiful. 🤩 Can't believe that after a ~decade of trying to make this happen, it turns out to be such a compact patch! 🤩
⇒ RTBC!
See y'all in #3308398: [PP-1] JS minification cache warming.
Comment #65
wim leersOkay, I was a bit too enthusiastic there 🙈🙈🙈
While struck by the elegance and compactness of the patch … I conveniently overlooked the fact that there is no test coverage yet. I think we should have simple test coverage to confirm that:
indeed results in comments, whitespace and optional braces getting stripped.
Comment #66
catchThere's some implicit whitespace removal with some of the test file changes, but yeah let's add a more explicit one.
Comment #67
nod_Looks good to me 😊
Comment #68
wim leersPerfect! 🤩 Even simpler test coverage than I dared hope for!
Comment #69
alexpottI wonder whether this cache is worth it? We're going to dump the results to disk and use that. I also wonder if this cache is going to cause pain if you change a JS file and change the js/css query string thing.
Comment #70
nod_In the situation where you have 10 different aggregates that all include the same js file (for some reason) that file would be minified the same way 10 different times. Since we can't predict the aggregates and how many are going to be built based on user permissions and such, having a cache makes sure we don't do work that could be prevented. And if your file is big we could easily save like a second overall by using the cache.
Does the query string work without clearing the cache today? the file name depends on the file contents so just changing the query string wouldn't update the content of the file. When I need to update aggregates I clear the cache, that'll work the same here. There is no more query string cache busting in the new urls so that's a hint it works differently as before no?
Comment #71
alexpottWhat I think is very different is if you clear the aggregates and do a deployment then JS files might not change. Thinking about this some more I think we should be using a filecache object here. That'll key using the mtime of the file - see \Drupal\Component\FileCache\FileCache::set() - I think doing that will prevent some really odd surprises.
Comment #72
nod_Ok I see the problem now, got bitten by it trying to debug some js and the agreggate not being generated correctly
Comment #73
nod_Tried to work out how to use FileCache, seems like we need to implement a DatabaseBackendCacheBackend for the FileCache so that the minified result is stored in the DB and not in acpu or in the static cache. Does that sound about right?
Comment #74
catchLooking at FileCache, first a re-roll.
Comment #75
catchShould look something like this. There are not that many files to minify and the minified versions should be in the hundreds of kilobytes, so I think we can just use the apcu backend, if that's a concern I think adding a database backend should be in a follow-up.
Comment #76
wim leersIsn't that already very quickly full on more complex sites?
And … "hundreds of kilobytes" really just depends on the amount of JS on a particular site, doesn't it? 😅
Comment #77
catchWell maybe but asset aggregation tends to be a lot of requests for a few minutes, then very few after that, so if it's happening after a cache clear and items then get expired that seems OK? I don't particularly mind adding a database FileCache backend but it seems out of scope here, would prefer to get this in without any caching at all than blocking it on implementations for unrelated components.
Yes, and it'll also be affected by #3308122: Pre-minify core JavaScript which will mean the logic here only happens as a fallback for contrib/custom JS that's not pre-minified.
Comment #79
catchTest failures are all unrelated, chromedriver is having bad day.
Comment #81
alexpott->get($cid)would be more consistent for me. So it matches the set lower down.Re #77 - how much does the cache actually save us?
Comment #82
catchFixing $cid.
I did some basic timings in #7 which should still be relevant for the current patch.
Any file that's 10-100ms the caching will be a net-positive if we get one or more cache hits.
However, the question then is what is the cache hit rate going to be like.
I took a look at the js directory of one production Drupal 9.4 site - this is a fairly complex site although it is not high-traffic.
js aggregates are kept around for 6 weeks with the current logic, so that gives us data since September 7th.
ajax.js was in 276 different aggregates produced since September 7th. This amounts to 27 seconds of minification time if we were minifying it each time.
This site has had a handful of deployments since September 7th, and the files are mostly created during deployments, then not until the next one. So you tend to get 5-20 aggregates per-deployment (anonymous, regular user, admin account, couple of different pages with different JavaScript on them etc.) then a gap in dates until the next tone.
If someone wants to do a comparison on a different site, I just did some grepping for toConsumableArray, this will tell you how many aggregates with that string were created in the past seven days:
On this site it was 44 in the past seven days.
Based on this I think it is reasonable to say that a file like ajax.js will probably get minified 5-10 times almost immediately after a deployment, and then barely touched until the next deployment. This means we'd save 500ms to 1s from caching minification of that file in total across aggregate requests, and if it then gets chucked out of the apcu cache later no-one will notice.
Given all that I don't think it's critical to add caching here at all, it's definitely a nice to have (and especially once we pre-minify core JavaScript since those won't need to be minified or cached at all), but it will slightly help the immediate cold-cache situation you get after deployments in those first few seconds/minutes.
Comment #84
catchComment #86
catchDiscussed the caching a bit more with @alexpott. Although it's likely it will reduce the number of times the same file gets minified, we also have other ways to do that:
- pre-minification of core JavaScript (so on-the-fly only runs for contrib and custom JavaScript)
- reducing the variations in aggregates (i.e. by making ordering more consistent)
Also the hit rate is always going to be low - a dozen or so hits at a time.
So... new version with caching removed, if it turns out we really do need it, we could add it in a follow-up.
Comment #87
nod_Still working as expected :)
+1 to remove caching, we have 6 months to fix if it ends up being an issue
Comment #88
wim leersMagnificently simple patch for an incredibly high-impact front-end performance improvement!
RTBC++
Although … my inner nerd would definitely like to see a test that proves that this saves at least X bytes with aggregation on vs aggregation off on
Standard's front page and/node/add/articleroute, which is possible in aFunctionalJavascripttest that inspectswindow.performance— see\Drupal\Tests\ckeditor5\FunctionalJavascript\MediaTest::getLastPreviewRequestTransferSize()and\Drupal\Tests\ckeditor5\FunctionalJavascript\AdminUiTest::getAjaxResponseCount()for examples. But that's just a nice-to-have.Heck, it'd just require additions to
\Drupal\Tests\standard\FunctionalJavascript\StandardJavascriptTest!Reason I'd like this: that way we'd have some "executable documentation" that proves the real-world impact of this on a fresh Drupal site 🤓
Comment #89
catchI think it could make sense to add that test coverage, but only if we weren't also planning to do #3308122: Pre-minify core JavaScript which would make the test coverage redundant again (i.e. that issue renders this code a no-op with only core JavaScript so there'd be no size reduction because nothing would be minified with Peast in core), given that I'm -1 on adding something we'll (hopefully) have to remove again in a few weeks/months.
Comment #90
wim leersD'oh, right — that makes sense! Although I guess we could simulate it by installing a test module whose JS we would not minify in #3308122 — thus proving it'd correctly help improve things for contrib. Then all that would change in the test coverage in #3308122 is that the expected minimum bytes saved would change!
Comment #91
longwaveAdded the dependency evaluation to https://www.drupal.org/about/core/policies/core-dependency-policies/depe...
Comment #92
nod_All that could be follow ups, let's not drag patches more than they need to. Much easier for people to get involved in an "add test" issue than a "100 comments and implementation discussion + add test issue"
Comment #93
longwaveRemoving this comment is out of scope?
Comment #94
catchJust putting back the stray comment how we found it.
@Wim I'm not sure how a test module would be particularly more realistic than adding a bigger source file to the minification unit test and comparing the bytes there. But yes let's discuss in a follow-up.
Comment #95
alexpottWe should add
'mck89/peast' => ['doc'],to\Drupal\Composer\Plugin\VendorHardening\Config::$defaultConfigComment #97
longwaveCommitted and pushed 79035ac12d to 10.1.x. Thanks!
Also published the change record.
edit: crosspost with @alexpott; let's do that in a followup, sorry.
Comment #99
catchComment #100
xjmThis was already added to the 10.1.0 release notes draft.
Comment #101
andypostthere's follow-up #3374660: Update mck89/peast composer dependency to 1.15.2
Comment #102
edysmpexample JS code:
```
if (true) {
const CMS = 'Drupal';
}
```
Peast parses to: `if (true) const CMS = 'Drupal';` which is: `SyntaxError: Unexpected token 'const'`
contrib code affected https://git.drupalcode.org/project/glightbox/-/blob/1.0.x/js/glightbox.j...
Comment #103
longwave@edysmp can you raise that at https://github.com/mck89/peast/issues? The Peast maintainer does not monitor this issue as far as I am aware.
constis block scoped in JavaScript, which means the variable is pointless (it goes out of scope on the next line) and there must be another side effect going on, but this probably still should be considered a bug.Comment #104
edysmp@longwave Thanks for taking a look. Post the issue in Peast project: https://github.com/mck89/peast/issues/60
Comment #105
edysmpFYI contrib module affected mentioned in #102 is no longer an issue. Fixed by https://www.drupal.org/project/glightbox/issues/3378356#comment-15174019
Comment #106
longwave@edysmp the Peast maintainer has fixed this bug and released a new version: https://github.com/mck89/peast/releases/tag/v1.15.3
Thanks for raising it!
Comment #107
edysmp@longwave Great! So, next is to update Peast here? Worth a follow-up at this moment?
Comment #108
anybodySorry to comment on this fixed issue, but I hope we can get a short information here, if this needs a follow-up issue.
In COOKiES (for GDPR) we need to disable preprocessing (
preprocess: false) because the JS files may not be aggregated to be able to block them.But setting
preprocess: falsehas the side-effect that the resulting JS wll also not be minified, while it would be totally okay aka "good" to have it minified.So I think it may need a more granular separation of "preprocess" / "aggregate" / "minified" to be able to solve such cases?