Skip to content

[TwigComponent] Improve performance for complex use-cases #2812

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

Closed
adri opened this issue Jun 2, 2025 · 13 comments
Closed

[TwigComponent] Improve performance for complex use-cases #2812

adri opened this issue Jun 2, 2025 · 13 comments

Comments

@adri
Copy link

adri commented Jun 2, 2025

Firstly, great work and nice to see a component based way of working for Symfony.

Scenario

Creating components like shadcn with Twig Components is great. However, when using a lot of them, the performance degrades quickly. Especially when adding popovers with buttons in them for each cell.

  1. There seems to be overhead with the rendering of each component
  2. The response size gets very large (4 MB in the example) even compressed it takes quite a while for the browser to download and parse

Reproducer

Image
  1. Checkout the reproducer
  2. composer install
  3. symfony server:start

Solutions

I'm probably switching back to simple HTML elements again (<button class="ghost">) and use Tailwinds @apply for different variants. Of course, I'd prefer to keep using the component approach.

Phoenix LiveView solved the 2. problem with an optimization, see Optimization #1: splitting statics from dynamics. This way the response size doesn't grow very large.

@adri
Copy link
Author

adri commented Jun 2, 2025

@Kocal I see you’re busy with building components like I did. I guess I don’t do something completely wrong 😉
https://github.com/symfony/ux/tree/2.x/src/Toolkit/kits/shadcn/docs/components

@Kocal
Copy link
Member

Kocal commented Jun 2, 2025

Thanks for the reproducer :) I will try to give Blackfire a try when having some free time.

@smnandre already worked on TwigComponent optimization, and IIRC, major performance impacts were found due to how event listeners worked.

@smnandre
Copy link
Member

smnandre commented Jun 2, 2025

Hello,

Regarding your 2nd point, TwigComponent only renders the HTML coded in your templates.

Image

If you wanted to reduce, I think using CSS here could make you gain at least 50% of the generated HTML size.

Regarding your 1st one, it does not surprise me that much in this situation to be honest.

  • LiveComponent are not made to render tens of thousands or nested components
  • Using Component for SVG icon will be less performant than using... UX Icons
  • html_cva and Taildwind merge have a real cost too
  • you have a very high usage of embeding components, and we need to say it often but components are not functions like in JS but classes, even services, with the associated pros and cons.

It is possible to run hundred of thousands of components in a page with no troubles.... when they are more "independant" than in your situation. As soon components are not "isolated pieces of" but "nested tree of" this comes with a cost, sadly.

I'm digging your article later today, it seems very interesting, thank you.

@adri
Copy link
Author

adri commented Jun 3, 2025

Thanks all for the comments!

No components test

I've started with migrating to HTML elements with classes to see what a potential optimum could be. On the no-components branch of the reproducer I switched the Button component to an HTML element + classes.

Twig Components with Tailwind classes
Response time: ~530ms
HTML: 4,8MB
CSS: 22kb

HTML elements with extracted Tailwind classes
Response time: ~230ms
HTML: 2,8MB
CSS: 26kb

SVG

In that branch I also switched to UX Icons, but I could not see a difference. They also render as SVG elements.
In the real project (not the reproducer) I implemented the helper to re-use icons which reduced the response size back then by about 100kb (9%).

Anonymous Components

Is it an idea to treat anonymous components differently? More like functions instead of services.

@smnandre
Copy link
Member

smnandre commented Jun 3, 2025

  Twig Components with Tailwind classes
  Response time: ~530ms
  HTML: 4,8MB
  CSS: 22kb
  
  HTML elements with extracted Tailwind classes
  Response time: ~230ms
  HTML: 2,8MB
  CSS: 26kb

I'm sorry I don't get this... 🤔

How can the HTML be different if you are writing the same HTML ? TwigComponent does not add a single tag or attribute you do not render yourself.

So it's either a question of tailwind_merge (not a Symfony UX package) or one with html_cva (a Twig extra package now)

....or am i missing something here ?

@adri
Copy link
Author

adri commented Jun 4, 2025

Sorry I should have been clearer! What this commit does is:

  1. Move Tailwind CSS to a CSS file that is shared.
    This is to reduce the response size. The rendered output changes from this:
    <button class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:cursor-no-drop disabled:text-gray-600 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground h-9 p-2">...
    To this:
    <button class="button variant-ghost size-sm p-2">
  2. No Twig component is used.
    So no event subscribers, template finding, tailwind_merge, html_cva.... This is of course less flexible, as I can't add any logic, other than what's possible with CSS only.
    <twig:Button ... />
    To this:
    <button ... />

I will continue doing the same for other components than Button.

In terms of solutions, I'm thinking:

  • Splitting static and dynamic parts of a template (e.g. part of the classes) could in theory allow us to reuse static ones like described in this blog post, addressing 1.
  • If we focus only on CSS, a static build step that automates what I now do manually, similar to compile-class functionality of unocss. It would convert inline-flex items-center justify-center gap-2 whitespace-nowrap into .<prefix>-<randomId> { @apply inline-flex items-center justify-center gap-2 whitespace-nowrap }. Exactly what I'm now doing manually.
  • Could the overhead of rendering anonymous component be reduced, by treating them differently than other components? E.g. not raising any events. This would address 2.

Curious what you think?

Blackfire seems paid now? I'll see if I can run SPX later for better data.

@adri
Copy link
Author

adri commented Jun 4, 2025

Tailwind Merge

Image

Found a few unnecessary instantiations of data in TailwindMerge, see

When I make them static I get a nice reduction in the reproducer:

Response time main: ~530ms
Response time main + statics: ~350ms

Response time no-components: ~230ms
Response time no-components + statics: ~165ms

@smnandre
Copy link
Member

smnandre commented Jun 4, 2025

static thing

Twig already differenciates a static string and an evaluated expression. And cannot return only a part of it so i'm not sure it would change anything here.

If we focus only on CSS ...

There is nothing related to CSS in TwigComponent, so this is not something we have any power.

tailwind_merge and html_cva are not from this repository, but if their author / organization can improve their performances that's a good news (not saying they are bad).

Could the overhead of rendering anonymous component be reduced, by treating them differently than other components? E.g. not raising any events. This would address 2.

Not now sadly, that would break the BC promise.

But this is something we will work on in 3.0 (and I have several POC / prototypes ready for this) 😄

@smnandre
Copy link
Member

smnandre commented Jun 4, 2025

By the way, you need to disable the Twig Component profiler to get accurate results (if not already done i mean).

Image

From what i'm seeing from your screenshot, 30% of the render time is taken by tailwind utilities.. so indeed i'd start there

@adri
Copy link
Author

adri commented Jun 4, 2025

@smnandre thanks for the comments!

Yes got (local) fixes for the Tailwind stuff and hope to contribute it back upstream 👍 The profiler is disabled, thanks! I do see indeed a big junk for event listener tracking in my real project where the profiler is enabled.
Still quite some time is overhead rendering Twig components.

I forgot to say, but I also looked into event listeners as it was hinted on before here #2812 (comment).

Got a nice performance boost by disabling event dispatching in ComponentRenderer and ComponentFactory (e.g. preMount and postMount). I only used anonymous components. At least for my use-case nothing changed visually or functionally. I think the reproducer version on main went from ~530ms to ~450ms.

My components look very similar to what @Kocal is building (also using tailwind_merge), see e.g.
https://github.com/symfony/ux/blob/2.x/src/Toolkit/kits/shadcn/templates/components/Button.html.twig

@adri
Copy link
Author

adri commented Jun 4, 2025

Going to close this as I think we discussed what's possible and I have a few improvements to do in other repos.

But this is something we will work on in 3.0 (and I have several POC / prototypes ready for this) 😄

Looking forward to this!

@adri adri closed this as completed Jun 4, 2025
@smnandre
Copy link
Member

smnandre commented Jun 4, 2025

If you want to get a big performance improvement for this scenario (a lot of embedding anonymous component with big graph): try uninstalling live component. And tell me what metrics you get :)

You are 100% right about the events, it's one of the things we cannot move for now without massive BC break... but something we need to change... without what any optimization is just very, very hard to get.

@Kocal
Copy link
Member

Kocal commented Jun 4, 2025

Opened gehrisandro/tailwind-merge-php#19 and #2821

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

No branches or pull requests

4 participants