Skip to content

Add fast paths for class/style string arguments in BlazeAttributeBag#168

Open
SanderMuller wants to merge 2 commits into
livewire:mainfrom
SanderMuller:autoresearch/attribute-bag-fast-paths
Open

Add fast paths for class/style string arguments in BlazeAttributeBag#168
SanderMuller wants to merge 2 commits into
livewire:mainfrom
SanderMuller:autoresearch/attribute-bag-fast-paths

Conversation

@SanderMuller

Copy link
Copy Markdown
Contributor

When merge(), class(), or style() receive a simple string argument (the most common usage), skip the general-purpose loops and handle the merge directly.

The shared class-merging logic is extracted into withMergedClass() to avoid duplication between merge() and class().

This replaces both PR #157 and #161 with a single combined PR.

@SanderMuller

Copy link
Copy Markdown
Contributor Author

/benchmark class

@SanderMuller

Copy link
Copy Markdown
Contributor Author

/benchmark merge

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Result: Class

Attempt Blade Blaze Improvement
#1 240.77ms 13.68ms 94.3%
#2 234.65ms 13.61ms 94.2%
#3 236.62ms 13.60ms 94.3%
#4 228.92ms 13.25ms 94.2%
#5 237.33ms 13.55ms 94.3%
#6 231.15ms 13.47ms 94.2%
#7 234.94ms 13.41ms 94.3%
#8 230.39ms 13.66ms 94.1%
#9 231.89ms 13.48ms 94.2%
#10 235.80ms 13.38ms 94.3%
Snapshot 235.27ms 17.91ms 92.4%
Result 234.80ms (~) 13.52ms (-24.5%) 94.2% (+1.8%)

Median of 10 attempts, 5000 iterations x 10 rounds, 31.77s total

To run a specific benchmark, comment /benchmark <name> where name is one of: attributes, aware, class, default, forwarding, merge, named-slots, no-attributes, slot

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Result: Merge

Attempt Blade Blaze Improvement
#1 170.01ms 11.64ms 93.2%
#2 166.45ms 11.74ms 92.9%
#3 165.58ms 11.71ms 92.9%
#4 169.72ms 11.69ms 93.1%
#5 169.87ms 11.62ms 93.2%
#6 167.13ms 11.70ms 93%
#7 169.59ms 11.65ms 93.1%
#8 169.31ms 11.62ms 93.1%
#9 166.73ms 11.73ms 93%
#10 166.29ms 11.73ms 92.9%
Snapshot 166.13ms 13.64ms 91.8%
Result 168.22ms (~) 11.70ms (-14.2%) 93% (+1.2%)

Median of 10 attempts, 5000 iterations x 10 rounds, 23.32s total

To run a specific benchmark, comment /benchmark <name> where name is one of: attributes, aware, class, default, forwarding, merge, named-slots, no-attributes, slot

@github-actions

github-actions Bot commented Mar 29, 2026

Copy link
Copy Markdown
Contributor

Benchmark Result: Default

Attempt Blade Blaze Improvement
#1 367.89ms 16.22ms 95.6%
#2 362.84ms 16.67ms 95.4%
#3 370.57ms 16.29ms 95.6%
#4 356.67ms 16.45ms 95.4%
#5 366.04ms 16.43ms 95.5%
#6 359.45ms 16.01ms 95.5%
#7 355.03ms 16.30ms 95.4%
#8 356.56ms 16.18ms 95.5%
#9 356.03ms 15.81ms 95.6%
#10 349.82ms 16.27ms 95.3%
Snapshot 360.66ms 16.37ms 95.5%
Result 358.06ms (~) 16.28ms (~) 95.5% (~)

Median of 10 attempts, 5000 iterations x 10 rounds, 47.62s total

To run a specific benchmark, comment /benchmark <name> where name is one of: attributes, aware, class, default, forwarding, merge, named-slots, no-attributes, slot

@ganyicz

ganyicz commented Mar 29, 2026

Copy link
Copy Markdown
Collaborator

Looks like this causes an attribute ordering issue in the FluxPro tests (you would need to dev-require Flux Pro with a valid license to run these):

Tests\FluxProTest > chart:

- <ui-chart wire:model="data" class="block [:where(&amp;)]:relative w-full aspect-2/1" wire:ignore.children >
+ <ui-chart class="block [:where(&amp;)]:relative w-full aspect-2/1" wire:model="data" wire:ignore.children >

Alternatively feel free to add a separate comparison test for this

When merge(), class(), or style() receive a simple string argument
(the most common usage), skip the general-purpose loops and handle
the merge directly.

The shared class-merging logic is extracted into withMergedClass()
to avoid duplication between merge() and class().

Uses array union to place the merged key first, matching the attribute
ordering of the general merge() path's array_merge($defaults, $attrs).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@SanderMuller SanderMuller force-pushed the autoresearch/attribute-bag-fast-paths branch from 22dea7d to c7e51ea Compare March 29, 2026 19:55
@SanderMuller

Copy link
Copy Markdown
Contributor Author

Looks like this causes an attribute ordering issue in the FluxPro tests (you would need to dev-require Flux Pro with a valid license to run these):

Tests\FluxProTest > chart:

- <ui-chart wire:model="data" class="block [:where(&amp;)]:relative w-full aspect-2/1" wire:ignore.children >
+ <ui-chart class="block [:where(&amp;)]:relative w-full aspect-2/1" wire:model="data" wire:ignore.children >

Alternatively feel free to add a separate comparison test for this

Thanks. I do have a Flux Pro license, but figured it would be good to have a CI test so I took the option to add a test for it (and make the code pass it)

The fast paths skipped merge()'s style normalization (trailing semicolon)
and appendable-first key ordering whenever the bag carried the other
appendable attribute, e.g.:

  <x-merge-class wire:model="data" style="color:blue" />
  Blade: class="..." style="color:blue;" wire:model="data"
  Blaze: class="..." wire:model="data" style="color:blue"

Guard the class fast paths on the absence of a style attribute (and vice
versa for style()), and handle falsy current style values ('' / '0') the
way merge() does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@calebporzio calebporzio left a comment

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.

Really nice work here — the fast path is where the time goes, and it shows: I benchmarked locally and got class 3.56ms → 2.73ms (-23%) and merge 3.19ms → 2.77ms (-13%) blaze time vs main, right in line with the CI numbers.

While reviewing I wrote a batch of Blade-vs-Blaze comparison probes against the fast paths and found a few divergences beyond the ordering case @ganyicz caught — they all boil down to the same root cause: vanilla merge() also normalizes/reorders the other appendable attribute when it's in the bag, and the fast paths skipped that. For example:

<x-merge-class wire:model="data" style="color:blue" />

Blade: <div class="default-class" style="color:blue;" wire:model="data"></div>
Blaze: <div class="default-class" wire:model="data" style="color:blue"></div>

(missing trailing ; on style + ordering), same story for ->class('...') with a style attribute present and ->style('...') with a class attribute present, plus a falsy-current-style edge (style="0" got dropped, style="" behaves differently than Blade).

Since the cheapest correct move is to just not take the fast path in those cases, I pushed 1e154c0 to your branch: guards the class fast paths on ! isset($this->attributes['style']) (and vice versa for style()), fixes the falsy-style handling, and adds comparison tests covering the combinations. All 14 of my probe cases now match Blade byte-for-byte, the full suite passes with this branch merged into current main (210 passed), and the benchmark win is unchanged (class 2.73ms, merge 2.78ms — the guards don't fire in the common case).

Feel free to rework my commit if you see a way to keep the fast path even when both appendables are present. Otherwise this looks good to me — thanks Sander!

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.

3 participants