Skip to content

Skip the colour regexp on comma-free shorthands, return sanitized media types as-is: −7% objects, −4% CPU - #195

Merged
grosser merged 2 commits into
premailer:masterfrom
czj:perf/shorthands-and-media-types-fewer-allocations
Oct 27, 2026
Merged

grosser merged 2 commits into
premailer:masterfrom
czj:perf/shorthands-and-media-types-fewer-allocations

Conversation

@czj

@czj czj commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

The smaller sister of #194, two independent bits that are each too small for a PR of their own.

Shorthands first. expand_dimensions_shorthand! ran RE_COLOUR, the alternation of every named colour, over each margin, padding and border value, only to collapse the spaces around commas inside rgb()/hsl(). Without a comma that replacement is the identity, so it's now skipped when the value has no ,, along with the dup it no longer needs. create_dimensions_shorthand!, compute_dimensions_shorthand and create_shorthand_properties! also stop building throwaway arrays, enumerators and zip pairs: they read the four sides directly and compare them with ==.

Then media types. add_rule_set! sanitizes the media types of every rule. For :all, or any symbol already in sanitized form, that meant to_s, gsub, strip! and to_sym just to get the same symbol back. Such a symbol is now returned as-is. UNSANITIZED_MEDIA_QUERY_RX lists exactly what the slow path would change: blank, padded, or not separated by single spaces. Two crumbs ride along: calculate_specificity counts ids with count('#'), and parse_selectors! maps in place.

Median of 8 interleaved runs, Ruby 4.0.9, premailer 1.30.0, nokogiri 1.19.4:

objects CPU
our 336 transactional emails through premailer 13,592 → 12,666 (−7%) −4%
Mastodon's mailer CSS, parsed then merged 40,702 → 37,496 (−8%) −5%
premailer's own test files 28,502 → 26,590 (−7%) −5%
rake benchmark, complex.css 8,459 → 7,941 (−6%) ≈

All three corpora give byte-for-byte identical output. The 360,000-input differential fuzz against master covers shorthands, sanitize_media_query on strings and symbols, specificity and selector parsing, with no difference found. Both new tests fail on master: creating margin and padding shorthands takes 39 objects there and 25 here, sanitizing :all takes 3 there and 0 here. The suite passes on Ruby 3.3 and 4.0, and RuboCop is clean.

Two heads-ups. Only CHANGELOG.md conflicts with #194, and I'll rebase whichever lands last. #184 also touches parse_selectors!, which makes one trivial line to resolve.

Pre-Merge Checklist

  • CHANGELOG.md updated with short summary

🤖 Generated with Claude Code, but prompted, read, and verified by @czj, a caring human :)

…ithout commas

expand_dimensions_shorthand! ran RE_COLOUR, the alternation of every named
colour, over each dimension value only to collapse spaces around commas in
rgb()/hsl(): without a comma that is the identity. Creating dimension and
list/background shorthands stops building throwaway arrays and zip pairs.

sanitize_media_query returns an already sanitized symbol as-is instead of
round-tripping :all through to_s/gsub/strip!/to_sym for every rule;
calculate_specificity counts ids with count('#') and RuleSet#initialize no
longer builds a selectors array only to replace it.

Inlining our 336 emails with premailer allocates 7% fewer objects and uses
4% less CPU, with byte-for-byte identical output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Parsing selectors before specificity, offset and filename were set changed
what a subclass overriding parse_selectors! could see, and which error bad
arguments raised, for a single saved array. GC.stat counts allocations of
every thread, so the helper keeps the minimum of three runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment on lines -649 to +641
return [:top] if values.values.uniq.count == 1
return [:top] if values[:top] == values[:right] && values[:right] == values[:bottom] && values[:bottom] == values[:left]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kinda sad ... maybe a comment about not using values.uniq.count because that is slower to make sure nobody reverts this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm the test covers that

assert_equal "@media aural and (device-aspect-ratio: 16/9) {\n body {\n color: black;\n }\n}\n", @cp.to_s
end

def test_sanitizing_an_already_sanitized_media_type_does_not_allocate

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

@grosser
grosser merged commit d4642fb into premailer:master Oct 27, 2026
5 checks passed
@czj
czj deleted the perf/shorthands-and-media-types-fewer-allocations branch October 27, 2026 04:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants