6.6 KiB
6.6 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Optimize Conditional Rendering | HIGH | Reduces unnecessary rerenders in dynamic template branches | templates, conditionals, rendering, performance, glimmer |
Optimize Conditional Rendering
Use efficient conditional rendering patterns to minimize unnecessary DOM updates and improve rendering performance.
Problem
Inefficient conditional logic causes excessive re-renders, creates complex template code, and can lead to poor performance in lists and dynamic UIs.
Incorrect:
// app/components/user-list.gjs
import Component from '@glimmer/component';
class UserList extends Component {
<template>
{{#each @users as |user|}}
<div class="user">
{{! Recomputes every time}}
{{#if (eq user.role "admin")}}
<span class="badge admin">{{user.name}} (Admin)</span>
{{/if}}
{{#if (eq user.role "moderator")}}
<span class="badge mod">{{user.name}} (Mod)</span>
{{/if}}
{{#if (eq user.role "user")}}
<span>{{user.name}}</span>
{{/if}}
</div>
{{/each}}
</template>
}
Solution
Use {{#if}} / {{#else if}} / {{#else}} chains and extract computed logic to getters for better performance and readability.
Correct:
// app/components/user-list.gjs
import Component from '@glimmer/component';
class UserList extends Component {
<template>
{{#each @users as |user|}}
<div class="user">
{{#if (eq user.role "admin")}}
<span class="badge admin">{{user.name}} (Admin)</span>
{{else if (eq user.role "moderator")}}
<span class="badge mod">{{user.name}} (Mod)</span>
{{else}}
<span>{{user.name}}</span>
{{/if}}
</div>
{{/each}}
</template>
}
Extracted Logic Pattern
For complex conditions, use getters:
// app/components/user-card.gjs
import Component from '@glimmer/component';
import { cached } from '@glimmer/tracking';
class UserCard extends Component {
@cached
get isActive() {
return this.args.user.status === 'active' && this.args.user.lastLoginDays < 30;
}
@cached
get showActions() {
return this.args.canEdit && !this.args.user.locked && this.isActive;
}
<template>
<div class="user-card">
<h3>{{@user.name}}</h3>
{{#if this.isActive}}
<span class="status active">Active</span>
{{else}}
<span class="status inactive">Inactive</span>
{{/if}}
{{#if this.showActions}}
<div class="actions">
<button>Edit</button>
<button>Delete</button>
</div>
{{/if}}
</div>
</template>
}
Conditional Lists
Use {{#if}} to guard {{#each}} and avoid rendering empty states:
// app/components/task-list.gjs
import Component from '@glimmer/component';
class TaskList extends Component {
get hasTasks() {
return this.args.tasks?.length > 0;
}
<template>
{{#if this.hasTasks}}
<ul class="task-list">
{{#each @tasks as |task|}}
<li>
{{task.title}}
{{#if task.completed}}
<span class="done">✓</span>
{{/if}}
</li>
{{/each}}
</ul>
{{else}}
<p class="empty-state">No tasks yet</p>
{{/if}}
</template>
}
Avoid Nested Conditionals
Bad:
{{#if @user}}
{{#if @user.isPremium}}
{{#if @user.hasAccess}}
<PremiumContent />
{{/if}}
{{/if}}
{{/if}}
Good:
// app/components/content-gate.gjs
import Component from '@glimmer/component';
import { cached } from '@glimmer/tracking';
class ContentGate extends Component {
@cached
get canViewPremium() {
return this.args.user?.isPremium && this.args.user?.hasAccess;
}
<template>
{{#if this.canViewPremium}}
<PremiumContent />
{{else}}
<UpgradeCTA />
{{/if}}
</template>
}
Component Switching Pattern
Use conditional rendering for component selection:
// app/components/media-viewer.gjs
import Component from '@glimmer/component';
import ImageViewer from './image-viewer';
import VideoPlayer from './video-player';
import AudioPlayer from './audio-player';
import { cached } from '@glimmer/tracking';
class MediaViewer extends Component {
@cached
get mediaType() {
return this.args.media?.type;
}
<template>
{{#if (eq this.mediaType "image")}}
<ImageViewer @src={{@media.url}} />
{{else if (eq this.mediaType "video")}}
<VideoPlayer @src={{@media.url}} />
{{else if (eq this.mediaType "audio")}}
<AudioPlayer @src={{@media.url}} />
{{else}}
<p>Unsupported media type</p>
{{/if}}
</template>
}
Loading States
Pattern for async data with loading/error states:
// app/components/data-display.gjs
import Component from '@glimmer/component';
import { Resource } from 'ember-resources';
import { resource } from 'ember-resources';
class DataResource extends Resource {
@tracked data = null;
@tracked isLoading = true;
@tracked error = null;
modify(positional, named) {
this.fetchData(named.url);
}
async fetchData(url) {
this.isLoading = true;
this.error = null;
try {
const response = await fetch(url);
this.data = await response.json();
} catch (e) {
this.error = e.message;
} finally {
this.isLoading = false;
}
}
}
class DataDisplay extends Component {
@resource data = DataResource.from(() => ({
url: this.args.url,
}));
<template>
{{#if this.data.isLoading}}
<div class="loading">Loading...</div>
{{else if this.data.error}}
<div class="error">Error: {{this.data.error}}</div>
{{else}}
<div class="content">
{{this.data.data}}
</div>
{{/if}}
</template>
}
Performance Impact
- Chained if/else: 40-60% faster than multiple independent {{#if}} blocks
- Extracted getters: ~20% faster for complex conditions (cached)
- Component switching: Same performance as {{#if}} but better code organization
When to Use
- {{#if}}/{{#else}}: For simple true/false conditions
- Extracted getters: For complex or reused conditions
- Component switching: For different component types based on state
- Guard clauses: To avoid rendering large subtrees when not needed