Slow application when using hundrets of components, other ways of encapsulation?

forstjiri
Member | 4
+
0
-

Hi,
we have a classic eshop-like catalogue where there is 120 products on the page.

We are separating presenter which used to be extremely long and with around 150 $this->template->xxx definitions.
What seemed to be the cleanest solution regarding code clarity and maintainability was to cut the page into many smaller components, so for the grid, that would be 120 TileComponents done with Multiplier and then some components nested inside.

This proved to be impossible as it slowed up more than 25%. Example code: https://gist.github.com/…68d1440ad0db

Has anyone else tried to achieve this and how did you solve it in the end? Thanks.

For now, the compromise seems to be to define an array filled with Data Transfer Objects and in for loop render block, but it doesn't solve isolation as nicely as the component.

Last edited by forstjiri (2023-10-30 15:11)

Marek Bartoš
Nette Blogger | 1173
+
+3
-

Components creation itself of course has small overhead. Unless you give us more informations it's hard to tell whether you could make some improvements or not – rendering a lot of empty components is not a real world example.

Good thing about components is they can be initialized lazily. For laziness, always use createComponent* methods and create component via generated DIC factory. Render will still be slower, but handling ajax signals and processing forms will be a lot faster.

You can also use profilling tools to check what takes the most time

Last edited by Marek Bartoš (2023-10-30 16:07)

m.brecher
Generous Backer | 758
+
-2
-

@forstjiri

to cut the page into many smaller components, so for the grid, that would be 120 TileComponents

For now, the compromise seems to be to define an array filled with Data Transfer Objects and in for loop render block, but it doesn't solve isolation as nicely as the component.

It seems you have one component repeatedly rendered in one page. There are two ways how to achieve this.

a) One approach is create approx. 120 instances of one component and render each of it (Multiplier?)

b) The other possibility is to have one instance of this component and render it in {foreach} in template repeatedly and for every single rendering supply the component proper data via passing parameters.

sample code to explain the idea:

{$foreach $products as $product}
    {control 'productControl', product: $product}
{/foreach}
class ProductControl extends UI\Control
{

	public function render(ActiveRow  $product)
    {
		$this->template->product = $product;

        $this->template->render(__DIR__.'productView.latte');
    }
}

This may help to reduce the server overload.

mystik
Member | 291
+
0
-

Creation of hundreds components should add only few ms overhead by itself. I guess either your page is extremely fast so few ms makes 25% slowdown. Or there is bottleneck somewhere else. Usuall suspect is loading components data one by one instead of loading all componenta data in one query and then use it to create individual components.

forstjiri
Member | 4
+
0
-

Thanks for the answers, here is the example repo: (not our actual code but simplified so the problem is visible)
https://bitbucket.org/…/src/master/

I cannot use lazyloading in this case. I tried profiling and it seemed that Control::render was the bottleneck.

Having the render method with argument of active row helped a little.

Creation of hundreds components should add only few ms overhead by itself. Even from my simple test when adding 500 components it raised from about ~3ms to ~30ms which may not seem like a lot in absolute numbers but it matters a lot to us.

My summary is that Components cannot be used in such numbers for performance heavy projects.

mystik
Member | 291
+
+1
-

Yeah about 5ms per hundred components seems reasonable. But if that is too much then components are not a good solution for you.

I think bottleneck in this case could be repeated initalization of latte template rendering. Using simple echo instead of latte could be solution if components are trivial but fot more complex components I would go for wrapper component with foreach.

mskocik
Member | 53
+
0
-

I had an use case where we I was working on “activity stream”. Under the hood it was simple grid component which handled filtering, signals and other interactivity stuff (collapse/expand, show edit form, toggle status, etc.), but it had separate (nested component) Renderer to render feed entries.

So instead of multiple components There was only one. And instead of subcomponents I was importing template with {define} blocks.
If you use similar technique, probably it will be enough also for your use case (I would really like to see the perf test result). This is how it may look like:

class Renderer extends Control
{
  private bool $init = false;

  private function initRenderer(): void {
    $this->init = true;
    // init globally used rendering related stuff
    $this->template->setFile(__DIR__ . '/Renderer.latte');
	// etc.
  }

  public function render(Entity $entity) {
    !$this->init && $this->initRenderer();
	$tpl = $this->template;
	// set specific params if needed...
    $tpl->setParameters([
      'entity' => $entity,
    ]);
    $tpl->render();
  }
}

Renderer.latte

{import '@blocks.latte'}

{include #header entity: $entity}
{include #main entity: $entity}
{include #footer entity: $entity}

@blocks.latte


{define header}...{/define}
{define main}...{/define}
{define footer}...{/define}

Last edited by mskocik (2023-10-31 12:11)

Marek Bartoš
Nette Blogger | 1173
+
0
-

Is bottleneck really the render, not initialization of Latte engine? It is repeated for every component and with a lot of Latte extensions it can take some time.

Did you run the test at least twice to ensure all templates are compiled? Also debug mode being enabled adds a lot of time to processing.

Last edited by Marek Bartoš (2023-10-31 13:05)

forstjiri
Member | 4
+
0
-

@mystik Reinicialization of latte is about half of the overhead of what i tried.

@mskocik Yes, working with blocks instead of components is much much faster. But the block in your initRenderer does nothing.

@MarekBartoš I ran the tests on our staging and on my copy at once with command
hey -n 100 -q 5 -c 4 https://www.0.devklarka.cz/panske-tenisky | grep -E 'Fastest|Total'
And also looked at tracy time for confirmation. For what i tried render is about half of the overhead (base ~200ms, with template reiinicialization: +~130ms vs with just one template instance: +~60ms)

Last edited by forstjiri (2023-11-02 09:41)