# Ember Best Practices **Version 1.0.0** Ember.js Community January 2026 > **Note:** > This document is mainly for agents and LLMs to follow when maintaining, > generating, or refactoring Ember.js codebases. Humans > may also find it useful, but guidance here is optimized for automation > and consistency by AI-assisted workflows. --- ## Abstract Comprehensive performance optimization and accessibility guide for Ember.js applications, designed for AI agents and LLMs. Contains 42 rules across 7 categories, prioritized by impact from critical (route loading optimization, build performance) to advanced patterns (Resources, ember-concurrency, modern testing, composition patterns, owner/linkage management). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation. Uses WarpDrive for modern data management, includes accessibility best practices leveraging ember-a11y-testing and other OSS tools, and comprehensive coverage of reactive composition, data derivation, controlled forms, conditional rendering, data requesting patterns, built-in helpers, and service architecture patterns. --- ## Table of Contents 1. [Route Loading and Data Fetching](#1-route-loading-and-data-fetching) — **CRITICAL** - 1.1 [Implement Smart Route Model Caching](#11-implement-smart-route-model-caching) - 1.2 [Parallel Data Loading in Model Hooks](#12-parallel-data-loading-in-model-hooks) - 1.3 [Use Loading Substates for Better UX](#13-use-loading-substates-for-better-ux) - 1.4 [Use Route-Based Code Splitting](#14-use-route-based-code-splitting) - 1.5 [Use Separate Route and Template Files](#15-use-separate-route-and-template-files) 2. [Build and Bundle Optimization](#2-build-and-bundle-optimization) — **CRITICAL** - 2.1 [Avoid Importing Entire Addon Namespaces](#21-avoid-importing-entire-addon-namespaces) - 2.2 [Lazy Load Heavy Dependencies](#22-lazy-load-heavy-dependencies) - 2.3 [Use Embroider Build Pipeline](#23-use-embroider-build-pipeline) 3. [Component and Reactivity Optimization](#3-component-and-reactivity-optimization) — **HIGH** - 3.1 [Avoid Constructors in Components](#31-avoid-constructors-in-components) - 3.2 [Avoid CSS Classes in Learning Examples](#32-avoid-css-classes-in-learning-examples) - 3.3 [Avoid Legacy Lifecycle Hooks (did-insert, will-destroy, did-update)](#33-avoid-legacy-lifecycle-hooks-did-insert-will-destroy-did-update) - 3.4 [Avoid Unnecessary Tracking](#34-avoid-unnecessary-tracking) - 3.5 [Build Reactive Chains with Dependent Getters](#35-build-reactive-chains-with-dependent-getters) - 3.6 [Component File Naming and Export Conventions](#36-component-file-naming-and-export-conventions) - 3.7 [Prefer Named Exports, Fallback to Default for Implicit Template Lookup](#37-prefer-named-exports-fallback-to-default-for-implicit-template-lookup) - 3.8 [Prevent Memory Leaks in Components](#38-prevent-memory-leaks-in-components) - 3.9 [Use {{on}} Modifier for Event Handling](#39-use-on-modifier-for-event-handling) - 3.10 [Use @cached for Expensive Getters](#310-use-cached-for-expensive-getters) - 3.11 [Use Class Fields for Component Composition](#311-use-class-fields-for-component-composition) - 3.12 [Use Component Composition Patterns](#312-use-component-composition-patterns) - 3.13 [Use Glimmer Components Over Classic Components](#313-use-glimmer-components-over-classic-components) - 3.14 [Use Native Forms with Platform Validation](#314-use-native-forms-with-platform-validation) - 3.15 [Use Strict Mode and Template-Only Components](#315-use-strict-mode-and-template-only-components) - 3.16 [Use Tracked Toolbox for Complex State](#316-use-tracked-toolbox-for-complex-state) - 3.17 [Validate Component Arguments](#317-validate-component-arguments) 4. [Accessibility Best Practices](#4-accessibility-best-practices) — **HIGH** - 4.1 [Announce Route Transitions to Screen Readers](#41-announce-route-transitions-to-screen-readers) - 4.2 [Form Labels and Error Announcements](#42-form-labels-and-error-announcements) - 4.3 [Keyboard Navigation Support](#43-keyboard-navigation-support) - 4.4 [Semantic HTML and ARIA Attributes](#44-semantic-html-and-aria-attributes) - 4.5 [Use ember-a11y-testing for Automated Checks](#45-use-ember-a11y-testing-for-automated-checks) 5. [Service and State Management](#5-service-and-state-management) — **MEDIUM-HIGH** - 5.1 [Cache API Responses in Services](#51-cache-api-responses-in-services) - 5.2 [Implement Robust Data Requesting Patterns](#52-implement-robust-data-requesting-patterns) - 5.3 [Manage Service Owner and Linkage Patterns](#53-manage-service-owner-and-linkage-patterns) - 5.4 [Optimize WarpDrive Queries](#54-optimize-warpdrive-queries) - 5.5 [Use Services for Shared State](#55-use-services-for-shared-state) 6. [Template Optimization](#6-template-optimization) — **MEDIUM** - 6.1 [Avoid Heavy Computation in Templates](#61-avoid-heavy-computation-in-templates) - 6.2 [Compose Helpers for Reusable Logic](#62-compose-helpers-for-reusable-logic) - 6.3 [Import Helpers Directly in Templates](#63-import-helpers-directly-in-templates) - 6.4 [No helper() Wrapper for Plain Functions](#64-no-helper-wrapper-for-plain-functions) - 6.5 [Optimize Conditional Rendering](#65-optimize-conditional-rendering) - 6.6 [Template-Only Components with In-Scope Functions](#66-template-only-components-with-in-scope-functions) - 6.7 [Use {{#each}} with @key for Lists](#67-use-each-with-key-for-lists) - 6.8 [Use {{#let}} to Avoid Recomputation](#68-use-let-to-avoid-recomputation) - 6.9 [Use {{fn}} for Partial Application Only](#69-use-fn-for-partial-application-only) - 6.10 [Use Helper Libraries Effectively](#610-use-helper-libraries-effectively) 7. [Performance Optimization](#7-performance-optimization) — **MEDIUM** - 7.1 [Use {{on}} Modifier Instead of Event Handler Properties](#71-use-on-modifier-instead-of-event-handler-properties) 8. [Testing Best Practices](#8-testing-best-practices) — **MEDIUM** - 8.1 [MSW (Mock Service Worker) Setup for Testing](#81-msw-mock-service-worker-setup-for-testing) - 8.2 [Provide DOM-Abstracted Test Utilities for Library Components](#82-provide-dom-abstracted-test-utilities-for-library-components) - 8.3 [Use Appropriate Render Patterns in Tests](#83-use-appropriate-render-patterns-in-tests) - 8.4 [Use Modern Testing Patterns](#84-use-modern-testing-patterns) - 8.5 [Use qunit-dom for Better Test Assertions](#85-use-qunit-dom-for-better-test-assertions) - 8.6 [Use Test Waiters for Async Operations](#86-use-test-waiters-for-async-operations) 9. [Tooling and Configuration](#9-tooling-and-configuration) — **MEDIUM** - 9.1 [VSCode Extensions and MCP Configuration for Ember Projects](#91-vscode-extensions-and-mcp-configuration-for-ember-projects) 10. [Advanced Patterns](#10-advanced-patterns) — **MEDIUM-HIGH** - 10.1 [Use Ember Concurrency Correctly - User Concurrency Not Data Loading](#101-use-ember-concurrency-correctly---user-concurrency-not-data-loading) - 10.2 [Use Ember Concurrency for User Input Concurrency](#102-use-ember-concurrency-for-user-input-concurrency) - 10.3 [Use Helper Functions for Reusable Logic](#103-use-helper-functions-for-reusable-logic) - 10.4 [Use Modifiers for DOM Side Effects](#104-use-modifiers-for-dom-side-effects) - 10.5 [Use Reactive Collections from @ember/reactive/collections](#105-use-reactive-collections-from-emberreactivecollections) --- ## 1. Route Loading and Data Fetching **Impact: CRITICAL** Efficient route loading and parallel data fetching eliminate waterfalls. Using route model hooks effectively and loading data in parallel yields the largest performance gains. ### 1.1 Implement Smart Route Model Caching **Impact: MEDIUM-HIGH (Reduce redundant API calls and improve UX)** Implement intelligent model caching strategies to reduce redundant API calls and improve user experience. **Incorrect: always fetches fresh data** ```javascript // app/routes/post.js import Route from '@ember/routing/route'; import { service } from '@ember/service'; export default class PostRoute extends Route { @service store; model(params) { // Always makes API call, even if we just loaded this post return this.store.request({ url: `/posts/${params.post_id}` }); } } ``` ```glimmer-js // app/templates/post.gjs ``` **Correct: with smart caching** ```javascript // app/routes/post.js import Route from '@ember/routing/route'; import { service } from '@ember/service'; export default class PostRoute extends Route { @service store; model(params) { // Check cache first const cached = this.store.cache.peek({ type: 'post', id: params.post_id, }); // Return cached if fresh (less than 5 minutes old) if (cached && this.isCacheFresh(cached)) { return cached; } // Fetch fresh data return this.store.request({ url: `/posts/${params.post_id}`, options: { reload: true }, }); } isCacheFresh(record) { const cacheTime = record.meta?.cachedAt || 0; const fiveMinutes = 5 * 60 * 1000; return Date.now() - cacheTime < fiveMinutes; } } ``` ```glimmer-js // app/templates/post.gjs ``` **Service-based caching layer:** ```javascript // app/services/post-cache.js import Service from '@ember/service'; import { service } from '@ember/service'; import { TrackedMap } from 'tracked-built-ins'; export default class PostCacheService extends Service { @service store; cache = new TrackedMap(); cacheTimes = new Map(); cacheTimeout = 5 * 60 * 1000; // 5 minutes async getPost(id, { forceRefresh = false } = {}) { const now = Date.now(); const cacheTime = this.cacheTimes.get(id) || 0; const isFresh = now - cacheTime < this.cacheTimeout; if (!forceRefresh && isFresh && this.cache.has(id)) { return this.cache.get(id); } const post = await this.store.request({ url: `/posts/${id}` }); this.cache.set(id, post); this.cacheTimes.set(id, now); return post; } invalidate(id) { this.cache.delete(id); this.cacheTimes.delete(id); } invalidateAll() { this.cache.clear(); this.cacheTimes.clear(); } } ``` ```javascript // app/routes/post.js import Route from '@ember/routing/route'; import { service } from '@ember/service'; export default class PostRoute extends Route { @service postCache; model(params) { return this.postCache.getPost(params.post_id); } // Refresh data when returning to route async activate() { super.activate(...arguments); const params = this.paramsFor('post'); await this.postCache.getPost(params.post_id, { forceRefresh: true }); } } ``` ```glimmer-js // app/templates/post.gjs ``` **Using query params for cache control:** ```javascript // app/routes/posts.js import Route from '@ember/routing/route'; import { service } from '@ember/service'; export default class PostsRoute extends Route { @service store; queryParams = { refresh: { refreshModel: true }, }; model(params) { const options = params.refresh ? { reload: true } : { backgroundReload: true }; return this.store.request({ url: '/posts', options, }); } } ``` ```glimmer-js // app/templates/posts.gjs ``` **Background refresh pattern:** ```javascript // app/routes/dashboard.js import Route from '@ember/routing/route'; import { service } from '@ember/service'; export default class DashboardRoute extends Route { @service store; async model() { // Return cached data immediately const cached = this.store.cache.peek({ type: 'dashboard' }); // Refresh in background this.store.request({ url: '/dashboard', options: { backgroundReload: true }, }); return cached || this.store.request({ url: '/dashboard' }); } } ``` ```glimmer-js // app/templates/dashboard.gjs ``` Smart caching reduces server load, improves perceived performance, and provides better offline support while keeping data fresh. Reference: [https://warp-drive.io/](https://warp-drive.io/) ### 1.2 Parallel Data Loading in Model Hooks **Impact: CRITICAL (2-10× improvement)** When fetching multiple independent data sources in a route's model hook, use `Promise.all()` or RSVP.hash() to load them in parallel instead of sequentially. `export default` in these route examples is intentional because route modules are discovered through resolver lookup. In hybrid `.gjs`/`.hbs` codebases, keep route defaults and add named exports only when you need explicit imports elsewhere. **Incorrect: sequential loading, 3 round trips** ```javascript // app/routes/dashboard.js import Route from '@ember/routing/route'; import { service } from '@ember/service'; export default class DashboardRoute extends Route { @service store; async model() { const user = await this.store.request({ url: '/users/me' }); const posts = await this.store.request({ url: '/posts?recent=true' }); const notifications = await this.store.request({ url: '/notifications?unread=true' }); return { user, posts, notifications }; } } ``` **Correct: parallel loading, 1 round trip** ```javascript // app/routes/dashboard.js import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { hash } from 'rsvp'; export default class DashboardRoute extends Route { @service store; model() { return hash({ user: this.store.request({ url: '/users/me' }), posts: this.store.request({ url: '/posts?recent=true' }), notifications: this.store.request({ url: '/notifications?unread=true' }), }); } } ``` Using `hash()` from RSVP allows Ember to resolve all promises concurrently, significantly reducing load time. ### 1.3 Use Loading Substates for Better UX **Impact: CRITICAL (Perceived performance improvement)** Implement loading substates to show immediate feedback while data loads, preventing blank screens and improving perceived performance. **Incorrect: no loading state** ```javascript // app/routes/posts.js export default class PostsRoute extends Route { async model() { return this.store.request({ url: '/posts' }); } } ``` **Correct: with loading substate** ```glimmer-js // app/routes/posts-loading.gjs import { LoadingSpinner } from './loading-spinner'; ``` ```javascript // app/routes/posts.js export default class PostsRoute extends Route { model() { // Return promise directly - Ember will show posts-loading template return this.store.request({ url: '/posts' }); } } ``` Ember automatically renders `{route-name}-loading` route templates while the model promise resolves, providing better UX without extra code. ### 1.4 Use Route-Based Code Splitting **Impact: CRITICAL (30-70% initial bundle reduction)** With Embroider's route-based code splitting, routes and their components are automatically split into separate chunks, loaded only when needed. **Incorrect: everything in main bundle** ```javascript // ember-cli-build.js const EmberApp = require('ember-cli/lib/broccoli/ember-app'); module.exports = function (defaults) { const app = new EmberApp(defaults, { // No optimization }); return app.toTree(); }; ``` **Correct: Embroider with Vite and route splitting** ```javascript // ember-cli-build.js const { Vite } = require('@embroider/vite'); module.exports = require('@embroider/compat').compatBuild(app, Vite, { staticAddonTestSupportTrees: true, staticAddonTrees: true, staticHelpers: true, staticModifiers: true, staticComponents: true, splitAtRoutes: ['admin', 'reports', 'settings'], // Routes to split }); ``` Embroider with `splitAtRoutes` creates separate bundles for specified routes, reducing initial load time by 30-70%. Reference: [https://github.com/embroider-build/embroider](https://github.com/embroider-build/embroider) ### 1.5 Use Separate Route and Template Files **Impact: MEDIUM-HIGH (Better code organization and maintainability)** Keep route logic in `app/routes/*.js` and route templates in `app/templates/*.gjs`. Route classes imported from `@ember/routing/route` do not support inline `