95 lines
2.9 KiB
Markdown
95 lines
2.9 KiB
Markdown
---
|
|
title: Avoid Heavy Computation in Templates
|
|
impact: MEDIUM
|
|
impactDescription: 40-60% reduction in render time
|
|
tags: templates, performance, getters, helpers
|
|
---
|
|
|
|
## Avoid Heavy Computation in Templates
|
|
|
|
Move expensive computations from templates to cached getters in the component class or in-scope functions for template-only components. Templates should only display data, not compute it. Keep templates easy for humans to read by minimizing nested function invocations.
|
|
|
|
**Why this matters:**
|
|
|
|
- Templates should be easy to read and understand
|
|
- Nested function calls create cognitive overhead
|
|
- Computations should be cached and reused, not recalculated on every render
|
|
- Template-only components (without `this`) need alternative patterns
|
|
|
|
**Incorrect (heavy computation in template):**
|
|
|
|
```glimmer-js
|
|
// app/components/stats.gjs
|
|
import { sum, map, div, max, multiply, sortBy } from '../helpers/math';
|
|
|
|
<template>
|
|
<div>
|
|
<p>Total: {{sum (map @items "price")}}</p>
|
|
<p>Average: {{div (sum (map @items "price")) @items.length}}</p>
|
|
<p>Max: {{max (map @items "price")}}</p>
|
|
|
|
{{#each (sortBy "name" @items) as |item|}}
|
|
<div>{{item.name}}: {{multiply item.price item.quantity}}</div>
|
|
{{/each}}
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
**Correct (computation in component with cached getters):**
|
|
|
|
```glimmer-js
|
|
// app/components/stats.gjs
|
|
import Component from '@glimmer/component';
|
|
import { cached } from '@glimmer/tracking';
|
|
|
|
export class Stats extends Component {
|
|
// @cached is useful when getters are accessed multiple times
|
|
// For single access, regular getters are sufficient
|
|
|
|
@cached
|
|
get total() {
|
|
return this.args.items.reduce((sum, item) => sum + item.price, 0);
|
|
}
|
|
|
|
get average() {
|
|
// No @cached needed if only accessed once in template
|
|
return this.args.items.length > 0 ? this.total / this.args.items.length : 0;
|
|
}
|
|
|
|
get maxPrice() {
|
|
return Math.max(...this.args.items.map((item) => item.price));
|
|
}
|
|
|
|
@cached
|
|
get sortedItems() {
|
|
// @cached useful here as it's used by itemsWithTotal
|
|
return [...this.args.items].sort((a, b) => a.name.localeCompare(b.name));
|
|
}
|
|
|
|
@cached
|
|
get itemsWithTotal() {
|
|
// @cached useful as accessed multiple times in {{#each}}
|
|
return this.sortedItems.map((item) => ({
|
|
...item,
|
|
total: item.price * item.quantity,
|
|
}));
|
|
}
|
|
|
|
<template>
|
|
<div>
|
|
<p>Total: {{this.total}}</p>
|
|
<p>Average: {{this.average}}</p>
|
|
<p>Max: {{this.maxPrice}}</p>
|
|
|
|
{{#each this.itemsWithTotal key="id" as |item|}}
|
|
<div>{{item.name}}: {{item.total}}</div>
|
|
{{/each}}
|
|
</div>
|
|
</template>
|
|
}
|
|
```
|
|
|
|
**Note on @cached**: Use `@cached` when a getter is accessed multiple times (like in `{{#each}}` loops or by other getters). For getters accessed only once, regular getters are sufficient and avoid unnecessary memoization overhead.
|
|
|
|
Moving computations to getters ensures they run only when dependencies change, not on every render. Templates remain clean and readable.
|