Skip to content

[LiveComponent] Skip the bracket pipeline for plain model names - #3790

Closed
Kocal wants to merge 1 commit into
symfony:3.xfrom
Kocal:perf/live-component-normalize-model-name
Closed

[LiveComponent] Skip the bracket pipeline for plain model names#3790
Kocal wants to merge 1 commit into
symfony:3.xfrom
Kocal:perf/live-component-normalize-model-name

Conversation

@Kocal

@Kocal Kocal commented Aug 15, 2026

Copy link
Copy Markdown
Member
Q A
Bug fix? no
New feature? no
Deprecations? no
Documentation? no
Issues -
License MIT

normalizeModelName() ran a replace(), a split(), a map() with a
second replace() per part and a join() on every call, even for a plain
name like query where the whole pipeline is a no-op.

It runs on every input event through getValueFromElement(), and twice per
ValueStore.set() since set() normalizes and then calls get(), which
normalizes again.

Return the name untouched when it contains neither [ nor ]. Names that
do use the bracket syntax take the same path as before.

400k calls: plain names ~45 ms -> ~12 ms, a realistic 80/20 mix ~43 ms ->
~17 ms, bracketed names unchanged (~84 ms). These are micro-seconds per
event in absolute terms, but the change is three lines and output is
identical.

Browser-side JavaScript, so there is no Blackfire profile for this one.
Benchmarked on node 22 with node bench.mjs, comparing both
implementations side by side:

function current(model) {
    return model.replace(/\[]$/, '').split('[').map((s) => s.replace(']', '')).join('.');
}

function candidate(model) {
    if (!model.includes('[') && !model.includes(']')) {
        return model;
    }

    return model.replace(/\[]$/, '').split('[').map((s) => s.replace(']', '')).join('.');
}

const plain = ['firstName', 'email', 'query', 'isEnabled', 'selectedId'];
const bracketed = ['user[firstName]', 'user[mailing][]', 'form[items][0][label]'];

for (const [label, names] of [
    ['plain only', plain],
    ['bracketed only', bracketed],
    ['mixed 80/20', [...plain, ...plain, ...plain, ...plain, ...bracketed]],
]) {
    for (const [impl, fn] of [['current', current], ['candidate', candidate]]) {
        for (const n of names) fn(n); // warm up

        const start = process.hrtime.bigint();
        for (let i = 0; i < 400000; i++) {
            fn(names[i % names.length]);
        }
        console.log(`${label} ${impl} -> ${(Number(process.hrtime.bigint() - start) / 1e6).toFixed(2)} ms`);
    }
}

A stray ] with no [ was the one input where the fast path could have
diverged, so it is now covered by a unit test.

Analysis, implementation and benchmarks by Claude Opus 5.

| Q              | A
| -------------- | ---
| Bug fix?       | no
| New feature?   | no
| Deprecations?  | no
| Documentation? | no
| Issues         | -
| License        | MIT

`normalizeModelName()` ran a `replace()`, a `split()`, a `map()` with a
second `replace()` per part and a `join()` on every call, even for a plain
name like `query` where the whole pipeline is a no-op.

It runs on every input event through `getValueFromElement()`, and twice per
`ValueStore.set()` since `set()` normalizes and then calls `get()`, which
normalizes again.

Return the name untouched when it contains neither `[` nor `]`. Names that
do use the bracket syntax take the same path as before.

400k calls: plain names ~45 ms -> ~12 ms, a realistic 80/20 mix ~43 ms ->
~17 ms, bracketed names unchanged (~84 ms). These are micro-seconds per
event in absolute terms, but the change is three lines and output is
identical.

Browser-side JavaScript, so there is no Blackfire profile for this one.
Benchmarked on node 22 with `node bench.mjs`, comparing both
implementations side by side:

```js
function current(model) {
    return model.replace(/\[]$/, '').split('[').map((s) => s.replace(']', '')).join('.');
}

function candidate(model) {
    if (!model.includes('[') && !model.includes(']')) {
        return model;
    }

    return model.replace(/\[]$/, '').split('[').map((s) => s.replace(']', '')).join('.');
}

const plain = ['firstName', 'email', 'query', 'isEnabled', 'selectedId'];
const bracketed = ['user[firstName]', 'user[mailing][]', 'form[items][0][label]'];

for (const [label, names] of [
    ['plain only', plain],
    ['bracketed only', bracketed],
    ['mixed 80/20', [...plain, ...plain, ...plain, ...plain, ...bracketed]],
]) {
    for (const [impl, fn] of [['current', current], ['candidate', candidate]]) {
        for (const n of names) fn(n); // warm up

        const start = process.hrtime.bigint();
        for (let i = 0; i < 400000; i++) {
            fn(names[i % names.length]);
        }
        console.log(`${label} ${impl} -> ${(Number(process.hrtime.bigint() - start) / 1e6).toFixed(2)} ms`);
    }
}
```

A stray `]` with no `[` was the one input where the fast path could have
diverged, so it is now covered by a unit test.

Analysis, implementation and benchmarks by Claude Opus 5.
@Kocal

Kocal commented Aug 15, 2026

Copy link
Copy Markdown
Member Author

Duplicate of #3786, same branch.

@Kocal Kocal closed this Aug 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📊 Packages dist files size difference

Thanks for the PR! Here is the difference in size of the packages dist files between the base branch and the PR.
Please review the changes and make sure they are expected.

FileBefore (Size / Gzip)After (Size / Gzip)
LiveComponent
live_controller.js 82.79 kB / 18.4 kB 82.85 kB0% / 18.41 kB0%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants