Thanks to visit codestin.com
Credit goes to github.com

Skip to content

CSS: Drop the cache in finalPropName #5583

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 25, 2024
Merged

Conversation

mgol
Copy link
Member

@mgol mgol commented Nov 18, 2024

Summary

The finalPropName util caches properties detected to require a vendor prefix. This used to cache unprefixed properties as well, but it was reported that this logic broke accidentally during a refactor. Since fewer & fewer properties require a vendor prefix and caching a few basic checks likely has negligible perf benefits, opt to saving a few bytes and remove the cache.

Ref gh-5582

Size comparison:

main @d5ebb464debab6ac39fe065e93c8a7ae1de8547e
   raw     gz Filename
    -9     -3 dist/jquery.min.js
    -9     -7 dist/jquery.slim.min.js
    -9     -4 dist-module/jquery.module.min.js
    -9     -7 dist-module/jquery.slim.module.min.js

Checklist

@mgol mgol self-assigned this Nov 18, 2024
@mgol mgol added the CSS label Nov 18, 2024
@vlakoff
Copy link

vlakoff commented Nov 18, 2024

I would prefer to keep the cache, but if you go this way instead, you could go even further and inline the vendorPropName() function into finalPropName().

Just be aware the name parameter is overwritten in vendorPropName(), so a new variable has to be introduced to preserve the original name.

Code suggestion
// Returns a potentially-mapped vendor-prefixed property
export function finalPropName( name ) {
    // Check for unprefixed property names
    if ( name in emptyStyle ) {
        return name;
    }

    // Check for vendor-prefixed property names
    var capName = name[ 0 ].toUpperCase() + name.slice( 1 ),
        i = cssPrefixes.length;
    while ( i-- ) {
        var prefixedName = cssPrefixes[ i ] + capName;
        if ( prefixedName in emptyStyle ) {
            return prefixedName;
        }
    }

    // If no matching property is found, return the original name
    return name;
}

Fun fact: I discussed the above function with an AI, and it proposed adding exactly the same cache we're talking about here, even though I hadn't asked for it. It even provided a flawless code for it.

@timmywil
Copy link
Member

The cache no longer provides much value (I'm not 100% sure it ever did). Ironically, the suggestion you got from AI could very well be from training on previous versions of jQuery. But, I like your suggestion to inline vendorPropName.

@vlakoff
Copy link

vlakoff commented Nov 18, 2024

I have the whole context of the conversation with the AI, and the code it generated has a distinctive coding style.

I'm certain the result doesn't derive from past jQuery code; instead, the AI figured out that the results could be cached and generated code that perfectly accomplishes it.

@timmywil
Copy link
Member

There's no way to be certain of that, even if the code formatting is different. The AI was likely trained on at least some jQuery code given its ubiquity, but that's just an educated guess. Still, it's irrelevant to question of the usefulness of the cache.

@mgol mgol marked this pull request as ready for review November 19, 2024 23:48
@mgol mgol added this to the 4.0.0 milestone Nov 19, 2024
@mgol
Copy link
Member Author

mgol commented Nov 19, 2024

Inlining the vendorPropName function causes an increase in size:

drop-finalPropName-cache @d426bda80dd8d460c070b8d70969a81602609477
   raw     gz Filename
    +2     +3 dist/jquery.min.js
    +2     +5 dist/jquery.slim.min.js
    +2     +4 dist-module/jquery.module.min.js
    +2     +6 dist-module/jquery.slim.module.min.js

@mgol
Copy link
Member Author

mgol commented Nov 19, 2024

Ah, no, this was because the dirty state has a different version string with .dirty at the end and that inflates the size... I thought we were handling this somehow, @timmywil? 🤔

The final size difference is in favor:

last run
   raw     gz Filename
   -10     -3 dist/jquery.min.js
   -10     -1 dist/jquery.slim.min.js
   -10     -2 dist-module/jquery.module.min.js
   -10      0 dist-module/jquery.slim.module.min.js

I updated the PR.

@mgol mgol force-pushed the drop-finalPropName-cache branch from d426bda to d1ce9e6 Compare November 19, 2024 23:57
@mgol
Copy link
Member Author

mgol commented Nov 20, 2024

@vlakoff Caching is not always beneficial. We have two possible outcomes:

  1. The final property is unprefixed. With the new implementation, we'll just invoke the name in emptyStyle check and return name. With the old one, we'd first check in the cache and return if present. Otherwise, we'd do the name in emptyStyle check and return name. So at the first time we have two checks and then only one - no better than when cache is missing!
  2. The final property is prefixed. With the new implementation, we'll do up to a few checks for each call. I doubt this will be on any way expensive - those are very simple checks. Also, the number of properties requiring a prefix gets smaller & smaller over time as new vendor-prefixed properties are no longer being introduced.

For these reasons, we felt 4.0.0 is a good version to drop this cache. We'll keep it in 3.x just in case it matters more for older browsers.

@vlakoff
Copy link

vlakoff commented Nov 20, 2024

Caution: The PR is currently erroneous.

As I wrote above:

Just be aware the name parameter is overwritten in vendorPropName(), so a new variable has to be introduced to preserve the original name.

Currently, the name parameter is altered in the while loop. Therefore, when returning it as a fallback (no property found, whether unprefixed or prefixed), it contains the last tried prefixed name, instead of the supplied input value.

@mgol
Copy link
Member Author

mgol commented Nov 20, 2024

Why do we need to preserve the original name?

@vlakoff
Copy link

vlakoff commented Nov 20, 2024

  1. The final property is unprefixed. With the new implementation, we'll just invoke the name in emptyStyle check and return name. With the old one, we'd first check in the cache and return if present. Otherwise, we'd do the name in emptyStyle check and return name. So at the first time we have two checks and then only one - no better than when cache is missing!

Lookup is faster on a plain object than on the style property (instance of CSSStyleDeclaration). Both are fast, but the former is significantly faster (sorry, already deleted my local benchmarks). I suppose the function may be called a lot, and gains add up.

  1. The final property is prefixed. With the new implementation, we'll do up to a few checks for each call. I doubt this will be on any way expensive - those are very simple checks. Also, the number of properties requiring a prefix gets smaller & smaller over time as new vendor-prefixed properties are no longer being introduced.

Agree that prefixed properties are becoming increasingly rare. Actually, I didn't even consider them when thinking about performance. What we care about are the unprefixed names, which make up 99% of the cases.

Why do we need to preserve the original name?

Fallback if the supplied value is not a recognized property name. I don't know where this happens in real situations, but I certainly would not risk removing this fallback.

@mgol
Copy link
Member Author

mgol commented Nov 20, 2024

It’d be good to see some benchmarks to see what numbers we’re talking about.

You’re right about the fallback; I need to check if there are any measurable consequences of this and if the answer’s yes, add a test for this.

@timmywil
Copy link
Member

.dirty at the end and that inflates the size... I thought we were handling this somehow

I thought so too.

@timmywil
Copy link
Member

Lookup is faster on a plain object than on the style property

I suspect you're right about this, and probably by a lot. But, the question isn't, which is faster? The question is, is it fast enough without the cache, which might just take up space? Considering we've been without the cache for most properties (i.e. the ones that don't have a vendor prefix) since 3.4.0 and haven't seen evidence that it affects real world use cases, I doubt adding back the cache is necessary.

@timmywil
Copy link
Member

timmywil commented Nov 20, 2024

Off topic, but the compare size version regex wasn't accounting for the .2 in the beta.2 version string. See #5584

@vlakoff
Copy link

vlakoff commented Nov 20, 2024

Good news, I found back my benchmark:

Benchmark code
( function () {
    const nb = 100000;

    const emptyStyle = document.createElement( "div" ).style;
    const finalProps = {};
    finalProps.color = "color";

    const T1 = new Date();
    for ( let i = nb; i--; ) {
        if ( "color" in finalProps ) {}
    }
    const T2 = new Date();
    for ( let i = nb; i--; ) {
        if ( "color" in emptyStyle ) {}
    }
    const T3 = new Date();

    console.log( T2 - T1 );
    console.log( T3 - T2 );
} )();

Results for 10,000,000 iterations (in ms, lower is better) on Chrome:

6
949

The number of iterations is quite high. Although the former is significantly faster, both are very fast.
For perspective, 10,000 accesses take about 1 ms.

Same benchmark (10,000,000 iterations) on Firefox:

215
362

… Agree the cache can be removed.

@mgol
Copy link
Member Author

mgol commented Nov 20, 2024

Regarding the benchmark, it's even better - we don't expose these APIs publicly so their raw execution doesn't matter - it only matters how it influences public APIs. The real difference would be even lower.

@mgol
Copy link
Member Author

mgol commented Nov 20, 2024

I started wondering - do we need to auto-add vendor prefixes in 4.x at all? In the past, this was a godsend - instead of calling the border-radius getter or setter 5 times, only one was required. Since that time, browser makes agreed to not introduce new vendor prefixes and the old ones gradually disappeared.

I run a small script in supported browsers to detect prefixed CSS props that don't have the identically named unprefixed version and I got the following:

browser webkit* Moz* ms*
Chrome 131.0.6778.86 70 0 0
Firefox 132.0.2. 14 25 0
Safari 18.1.1 82 0 0
IE 11 0 0 55

If you look closer, e.g. at Chrome, you'll notice lots of these properties fall into one of the following buckets:

  • like -webkit-border-before: the unprefixed version is border-block-start - calling the setter with border-block-start won't pick up prefixed values and calling it with border-before would look invalid as there's no such property and there are no plans for it. Also, only the -webkit-prefixed version is supported anywhere and other versions are not likely to show up.
  • like -webkit-box-align: legacy API; unprefixed versions use different names & syntax
  • like -webkit-line-clamp: uses the -webkit prefix everywhere, including in Firefox - it should just be used directly as prefixed as there's no unprefixed version - and even if one arrives, it will work differently

There's also user-select which requires the -webkit- prefix in Safari & -ms- in IE, but those APIs seem quite uncommon on those lists.

@mgol
Copy link
Member Author

mgol commented Nov 20, 2024

More details in a separate comment, so that it's easier to skip it:

The script I used for the detection
(() => {
'use strict';

const cssPrefixes = ['webkit', 'Moz', 'ms'];

const prefixedProps = new Map();

const emptyStyle = document.createElement('div').style;

function unprefix(prop, prefix) {
    const withoutPrefix = prop.slice(prefix.length);
    return withoutPrefix[ 0 ].toLowerCase() + withoutPrefix.slice( 1 );
}

for (const prefix of cssPrefixes) {
    prefixedProps.set(prefix, new Set());
}

for (const prop in emptyStyle) {
    for (const prefix of cssPrefixes) {
        if (prop.startsWith(prefix)
                && !(unprefix(prop, prefix) in emptyStyle)) {
            prefixedProps.get(prefix).add(prop);
        }
    }
}

for (const [prefix, propsForPrefix] of prefixedProps) {
    console.log(`props for prefix ${prefix}`, propsForPrefix);
}

})();
The list of detected `-webkit-` properties in Chrome
  • -webkit-border-after
  • -webkit-border-after-color
  • -webkit-border-after-style
  • -webkit-border-after-width
  • -webkit-border-before
  • -webkit-border-before-color
  • -webkit-border-before-style
  • -webkit-border-before-width
  • -webkit-border-end
  • -webkit-border-end-color
  • -webkit-border-end-style
  • -webkit-border-end-width
  • -webkit-border-horizontal-spacing
  • -webkit-border-start
  • -webkit-border-start-color
  • -webkit-border-start-style
  • -webkit-border-start-width
  • -webkit-border-vertical-spacing
  • -webkit-box-align
  • -webkit-box-direction
  • -webkit-box-flex
  • -webkit-box-ordinal-group
  • -webkit-box-orient
  • -webkit-box-pack
  • -webkit-box-reflect
  • -webkit-column-break-after
  • -webkit-column-break-before
  • -webkit-column-break-inside
  • -webkit-font-smoothing
  • -webkit-line-clamp
  • -webkit-locale
  • -webkit-logical-height
  • -webkit-logical-width
  • -webkit-margin-after
  • -webkit-margin-before
  • -webkit-margin-end
  • -webkit-margin-start
  • -webkit-mask-box-image
  • -webkit-mask-box-image-outset
  • -webkit-mask-box-image-repeat
  • -webkit-mask-box-image-slice
  • -webkit-mask-box-image-source
  • -webkit-mask-box-image-width
  • -webkit-mask-position-x
  • -webkit-mask-position-y
  • -webkit-max-logical-height
  • -webkit-max-logical-width
  • -webkit-min-logical-height
  • -webkit-min-logical-width
  • -webkit-padding-after
  • -webkit-padding-before
  • -webkit-padding-end
  • -webkit-padding-start
  • -webkit-perspective-origin-x
  • -webkit-perspective-origin-y
  • -webkit-print-color-adjust
  • -webkit-rtl-ordering
  • -webkit-tap-highlight-color
  • -webkit-text-combine
  • -webkit-text-decorations-in-effect
  • -webkit-text-fill-color
  • -webkit-text-security
  • -webkit-text-stroke
  • -webkit-text-stroke-color
  • -webkit-text-stroke-width
  • -webkit-transform-origin-x
  • -webkit-transform-origin-y
  • -webkit-transform-origin-z
  • -webkit-user-drag
  • -webkit-user-modify
The list of detected `-moz-` properties in Firefox
  • -moz-box-align
  • -moz-box-direction
  • -moz-box-orient
  • -moz-box-pack
  • -moz-float-edge
  • -moz-orient
  • -moz-osx-font-smoothing
  • -moz-text-size-adjust
  • -moz-user-input
  • -moz-window-dragging
  • -moz-force-broken-image-icon
  • -moz-box-ordinal-group
  • -moz-box-flex
  • -moz-border-end-style
  • -moz-border-start-style
  • -moz-margin-end
  • -moz-margin-start
  • -moz-border-end-width
  • -moz-border-start-width
  • -moz-padding-end
  • -moz-padding-start
  • -moz-border-end-color
  • -moz-border-start-color
  • -moz-border-start
  • -moz-border-end
The list of detected `-webkit-` properties in Firefox
  • -webkit-box-align
  • -webkit-box-direction
  • -webkit-box-orient
  • -webkit-box-pack
  • -webkit-font-smoothing
  • -webkit-text-size-adjust
  • -webkit-line-clamp
  • -webkit-text-security
  • -webkit-box-ordinal-group
  • -webkit-box-flex
  • -webkit-text-stroke-width
  • -webkit-text-fill-color
  • -webkit-text-stroke-color
  • -webkit-text-stroke
The list of detected `-webkit-` properties in Safari
  • -webkit-border-after
  • -webkit-border-after-color
  • -webkit-border-after-style
  • -webkit-border-after-width
  • -webkit-border-before
  • -webkit-border-before-color
  • -webkit-border-before-style
  • -webkit-border-before-width
  • -webkit-border-end
  • -webkit-border-end-color
  • -webkit-border-end-style
  • -webkit-border-end-width
  • -webkit-border-horizontal-spacing
  • -webkit-border-start
  • -webkit-border-start-color
  • -webkit-border-start-style
  • -webkit-border-start-width
  • -webkit-border-vertical-spacing
  • -webkit-box-align
  • -webkit-box-decoration-break
  • -webkit-box-direction
  • -webkit-box-flex
  • -webkit-box-flex-group
  • -webkit-box-lines
  • -webkit-box-ordinal-group
  • -webkit-box-orient
  • -webkit-box-pack
  • -webkit-box-reflect
  • -webkit-column-axis
  • -webkit-column-break-after
  • -webkit-column-break-before
  • -webkit-column-break-inside
  • -webkit-column-progression
  • -webkit-cursor-visibility
  • -webkit-font-smoothing
  • -webkit-hyphenate-limit-after
  • -webkit-hyphenate-limit-before
  • -webkit-hyphenate-limit-lines
  • -webkit-initial-letter
  • -webkit-line-align
  • -webkit-line-box-contain
  • -webkit-line-clamp
  • -webkit-line-grid
  • -webkit-line-snap
  • -webkit-locale
  • -webkit-logical-height
  • -webkit-logical-width
  • -webkit-margin-after
  • -webkit-margin-before
  • -webkit-margin-end
  • -webkit-margin-start
  • -webkit-mask-box-image
  • -webkit-mask-box-image-outset
  • -webkit-mask-box-image-repeat
  • -webkit-mask-box-image-slice
  • -webkit-mask-box-image-source
  • -webkit-mask-box-image-width
  • -webkit-mask-position-x
  • -webkit-mask-position-y
  • -webkit-mask-source-type
  • -webkit-max-logical-height
  • -webkit-max-logical-width
  • -webkit-min-logical-height
  • -webkit-min-logical-width
  • -webkit-nbsp-mode
  • -webkit-padding-after
  • -webkit-padding-before
  • -webkit-padding-end
  • -webkit-padding-start
  • -webkit-rtl-ordering
  • -webkit-ruby-position
  • -webkit-text-combine
  • -webkit-text-decorations-in-effect
  • -webkit-text-fill-color
  • -webkit-text-security
  • -webkit-text-stroke
  • -webkit-text-stroke-color
  • -webkit-text-stroke-width
  • -webkit-text-zoom
  • -webkit-user-drag
  • -webkit-user-modify
  • -webkit-user-select
The list of detected `-ms-` properties in IE 11
  • -ms-block-progression
  • -ms-interpolation-mode
  • -ms-content-zoom-chaining
  • -ms-content-zoom-limit
  • -ms-content-zoom-limit-max
  • -ms-content-zoom-limit-min
  • -ms-content-zoom-snap
  • -ms-content-zoom-snap-points
  • -ms-content-zoom-snap-type
  • -ms-content-zooming
  • -ms-flex-align
  • -ms-flex-item-align
  • -ms-flex-line-pack
  • -ms-flex-negative
  • -ms-flex-order
  • -ms-flex-pack
  • -ms-flex-positive
  • -ms-flex-preferred-size
  • -ms-flow-from
  • -ms-flow-into
  • -ms-grid-column
  • -ms-grid-column-align
  • -ms-grid-column-span
  • -ms-grid-columns
  • -ms-grid-row
  • -ms-grid-row-align
  • -ms-grid-row-span
  • -ms-grid-rows
  • -ms-high-contrast-adjust
  • -ms-hyphenate-limit-chars
  • -ms-hyphenate-limit-lines
  • -ms-hyphenate-limit-zone
  • -ms-hyphens
  • -ms-ime-align
  • -ms-overflow-style
  • -ms-scroll-chaining
  • -ms-scroll-limit
  • -ms-scroll-limit-x-max
  • -ms-scroll-limit-x-min
  • -ms-scroll-limit-y-max
  • -ms-scroll-limit-y-min
  • -ms-scroll-rails
  • -ms-scroll-snap-points-x
  • -ms-scroll-snap-points-y
  • -ms-scroll-snap-type
  • -ms-scroll-snap-x
  • -ms-scroll-snap-y
  • -ms-scroll-translation
  • -ms-text-combine-horizontal
  • -ms-text-size-adjust
  • -ms-touch-select
  • -ms-user-select
  • -ms-wrap-flow
  • -ms-wrap-margin
  • -ms-wrap-through

@vlakoff
Copy link

vlakoff commented Nov 20, 2024

I have an infamous example: user-select on Safari.

  • Chrome:
    • supported -webkit- prefixed since 2008-12-11
    • supported unprefixed since 2016-10-12
  • Firefox:
    • supported -moz- prefixed since 2004-11-09
    • supported -webkit- prefixed since 2016-09-20
    • supported unprefixed since 2019-09-03
  • Safari:
    • supported -webkit- prefixed since 2007-10-26
    • still not supported unprefixed 🤡

Refs:

@vlakoff
Copy link

vlakoff commented Nov 20, 2024

I just noticed you already mentioned that one specifically:

There's also user-select which requires the -webkit- prefix in Safari & -ms- in IE, but those APIs seem quite uncommon on those lists.

Personally, I use this property quite often, and without jQuery, I have to handle both the unprefixed and -webkit- prefixed properties, because of Safari. I have tried reporting this issue to the developers a few times.

@mgol
Copy link
Member Author

mgol commented Nov 20, 2024

Yeah, user-select is a bummer. It's possible we'll have to wait with this for 5.0.0. I still created an issue and marked it for the team discussion: #5585.

@mgol
Copy link
Member Author

mgol commented Nov 21, 2024

I reverted locally to the main implementation with cache removed, i.e. with the vendorPropName function still existed and then to one with a single function. The latter showed the following size comparison (after applying PR #5586):

drop-finalPropName-cache
   raw     gz     br Filename
    -5     +1    +11 dist/jquery.min.js
    -5     +1    +13 dist/jquery.slim.min.js
    -5     +2    -60 dist-module/jquery.module.min.js
    -5     +1     -3 dist-module/jquery.slim.module.min.js

Fluctuations in Brotli sizes are pretty interesting. But the more popular no-module builds are larger, so using two functions makes sense, I think.

@mgol mgol force-pushed the drop-finalPropName-cache branch from d1ce9e6 to 0aedc9d Compare November 21, 2024 10:44
@mgol
Copy link
Member Author

mgol commented Nov 21, 2024

PR updated.

@mgol mgol requested a review from timmywil November 21, 2024 10:45
@mgol mgol requested a review from timmywil November 21, 2024 17:20
mgol added 2 commits November 26, 2024 00:07
The `finalPropName` util caches properties detected to require a vendor
prefix. This used to cache unprefixed properties as well, but it was
reported that this logic broke accidentally during a refactor. Since
fewer & fewer properties require a vendor prefix and caching a few
basic checks likely has negligible perf benefits, opt to saving a few
bytes and remove the cache.

Ref jquerygh-5582
@mgol mgol force-pushed the drop-finalPropName-cache branch from e2e0538 to 4c97827 Compare November 25, 2024 23:07
@mgol mgol removed the Needs review label Nov 25, 2024
@mgol mgol merged commit 640d582 into jquery:main Nov 25, 2024
17 checks passed
@mgol mgol deleted the drop-finalPropName-cache branch November 25, 2024 23:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Development

Successfully merging this pull request may close these issues.

3 participants