diff --git a/.agents/skills/ember-best-practices/AGENTS.md b/.agents/skills/ember-best-practices/AGENTS.md index 7895aed..9f6d34d 100644 --- a/.agents/skills/ember-best-practices/AGENTS.md +++ b/.agents/skills/ember-best-practices/AGENTS.md @@ -24,8 +24,8 @@ Comprehensive performance optimization and accessibility guide for Ember.js appl - 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 Templates with Co-located Syntax](#14-use-route-templates-with-co-located-syntax) - - 1.5 [Use Route-Based Code Splitting](#15-use-route-based-code-splitting) + - 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) @@ -105,8 +105,8 @@ Implement intelligent model caching strategies to reduce redundant API calls and **Incorrect: always fetches fresh data** -```glimmer-js -// app/routes/post.gjs +```javascript +// app/routes/post.js import Route from '@ember/routing/route'; import { service } from '@ember/service'; @@ -117,21 +117,24 @@ export default class PostRoute extends Route { // 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** -```glimmer-js -// app/routes/post.gjs +```javascript +// app/routes/post.js import Route from '@ember/routing/route'; import { service } from '@ember/service'; @@ -162,21 +165,66 @@ export default class PostRoute extends Route { const fiveMinutes = 5 * 60 * 1000; return Date.now() - cacheTime < fiveMinutes; } - - } ``` +```glimmer-js +// app/templates/post.gjs + +``` + **Service-based caching layer:** -```glimmer-js -// app/routes/post.gjs +```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'; @@ -193,21 +241,24 @@ export default class PostRoute extends Route { 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:** -```glimmer-js -// app/routes/posts.gjs +```javascript +// app/routes/posts.js import Route from '@ember/routing/route'; import { service } from '@ember/service'; @@ -226,28 +277,31 @@ export default class PostsRoute extends Route { options, }); } - - } ``` +```glimmer-js +// app/templates/posts.gjs + +``` + **Background refresh pattern:** -```glimmer-js -// app/routes/dashboard.gjs +```javascript +// app/routes/dashboard.js import Route from '@ember/routing/route'; import { service } from '@ember/service'; @@ -266,17 +320,20 @@ export default class DashboardRoute extends Route { 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/) @@ -351,6 +408,18 @@ export default class PostsRoute extends Route { **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 { @@ -363,108 +432,7 @@ export default class PostsRoute extends Route { Ember automatically renders `{route-name}-loading` route templates while the model promise resolves, providing better UX without extra code. -### 1.4 Use Route Templates with Co-located Syntax - -**Impact: MEDIUM-HIGH (Better code organization and maintainability)** - -Use co-located route templates with modern gjs syntax for better organization and maintainability. - -**Incorrect: separate template file - old pattern** - -```glimmer-js -// app/routes/posts.js (separate file) -import Route from '@ember/routing/route'; - -export default class PostsRoute extends Route { - model() { - return this.store.request({ url: '/posts' }); - } -} - -// app/templates/posts.gjs (separate template file) - -``` - -**Correct: co-located route template** - -```glimmer-js -// app/routes/posts.gjs -import Route from '@ember/routing/route'; - -export default class PostsRoute extends Route { - model() { - return this.store.request({ url: '/posts' }); - } - - -} -``` - -**With loading and error states:** - -```glimmer-js -// app/routes/posts.gjs -import Route from '@ember/routing/route'; -import { service } from '@ember/service'; - -export default class PostsRoute extends Route { - @service store; - - model() { - return this.store.request({ url: '/posts' }); - } - - -} -``` - -**Template-only routes:** - -```glimmer-js -// app/routes/about.gjs - -``` - -Co-located route templates keep route logic and presentation together, making the codebase easier to navigate and maintain. - -Reference: [https://guides.emberjs.com/release/routing/](https://guides.emberjs.com/release/routing/) - -### 1.5 Use Route-Based Code Splitting +### 1.4 Use Route-Based Code Splitting **Impact: CRITICAL (30-70% initial bundle reduction)** @@ -505,6 +473,114 @@ Embroider with `splitAtRoutes` creates separate bundles for specified routes, re 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 `