angular-best-practices
Angularのパフォーマンス最適化とベストプラクティスをガイドします。Angularコードの新規作成・レビュー・リファクタリング時に、最適なパフォーマンス・バンドルサイズ・レンダリング効率を実現するために活用してください。
description の原文を見る
Angular performance optimization and best practices guide. Use when writing, reviewing, or refactoring Angular code for optimal performance, bundle size, and rendering efficiency.
SKILL.md 本文
Angular ベストプラクティス
Angular アプリケーションの包括的なパフォーマンス最適化ガイド。パフォーマンスボトルネックの排除、バンドル最適化、レンダリング改善のための優先度付けルールを含みます。
使用するタイミング
以下の場合にこれらのガイドラインを参照してください:
- 新しい Angular コンポーネントやページを作成する
- データ取得パターンを実装する
- パフォーマンスの問題がないかコードをレビューする
- 既存の Angular コードをリファクタリングする
- バンドルサイズやロード時間を最適化する
- SSR/ハイドレーションを設定する
優先度別ルールカテゴリ
| 優先度 | カテゴリ | 影響 | 注力点 |
|---|---|---|---|
| 1 | 変更検出 | 重大 | Signals、OnPush、Zoneless |
| 2 | 非同期ウォーターフォール | 重大 | RxJS パターン、SSR プリロード |
| 3 | バンドル最適化 | 重大 | 遅延ロード、ツリーシェイキング |
| 4 | レンダリングパフォーマンス | 高 | @defer、trackBy、仮想化 |
| 5 | サーバーサイドレンダリング | 高 | ハイドレーション、プリレンダリング |
| 6 | テンプレート最適化 | 中 | 制御フロー、パイプ |
| 7 | 状態管理 | 中 | Signal パターン、セレクタ |
| 8 | メモリ管理 | 低~中 | クリーンアップ、サブスクリプション |
1. 変更検出 (重大)
OnPush 変更検出を使用する
// 正解 - OnPush と Signals
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<div>{{ count() }}</div>`,
})
export class CounterComponent {
count = signal(0);
}
// 間違い - デフォルト変更検出
@Component({
template: `<div>{{ count }}</div>`, // 毎サイクル検査される
})
export class CounterComponent {
count = 0;
}
可変プロパティより Signal を推奨する
// 正解 - Signal は正確な更新をトリガーします
@Component({
template: `
<h1>{{ title() }}</h1>
<p>Count: {{ count() }}</p>
`,
})
export class DashboardComponent {
title = signal("Dashboard");
count = signal(0);
}
// 間違い - 可変プロパティは zone.js チェックが必要です
@Component({
template: `
<h1>{{ title }}</h1>
<p>Count: {{ count }}</p>
`,
})
export class DashboardComponent {
title = "Dashboard";
count = 0;
}
新規プロジェクトで Zoneless を有効化する
// main.ts - Zoneless Angular (v20+)
bootstrapApplication(AppComponent, {
providers: [provideZonelessChangeDetection()],
});
メリット:
- 非同期 API への zone.js パッチなし
- バンドルが小さい (~15KB 削減)
- デバッグのためのクリーンなスタックトレース
- マイクロフロントエンド互換性の向上
2. 非同期操作とウォーターフォール (重大)
順序ある データ取得を排除する
// 間違い - ネストされたサブスクリプションがウォーターフォールを作成します
this.route.params.subscribe((params) => {
// 1. params を待つ
this.userService.getUser(params.id).subscribe((user) => {
// 2. user を待つ
this.postsService.getPosts(user.id).subscribe((posts) => {
// 3. posts を待つ
});
});
});
// 正解 - forkJoin で並列実行
forkJoin({
user: this.userService.getUser(id),
posts: this.postsService.getPosts(id),
}).subscribe((data) => {
// 並列で取得される
});
// 正解 - switchMap で依存呼び出しをフラット化
this.route.params
.pipe(
map((p) => p.id),
switchMap((id) => this.userService.getUser(id)),
)
.subscribe();
SSR でのクライアント側ウォーターフォールを回避する
// 正解 - リゾルバーまたはブロッキングハイドレーションを使用して重要データを取得
export const route: Route = {
path: "profile/:id",
resolve: { data: profileResolver }, // ナビゲーション前にサーバーで取得
component: ProfileComponent,
};
// 間違い - コンポーネント初期化時にデータを取得
class ProfileComponent implements OnInit {
ngOnInit() {
// JS ロード後およびコンポーネントレンダリング後に開始
this.http.get("/api/profile").subscribe();
}
}
3. バンドル最適化 (重大)
ルートを遅延ロードする
// 正解 - 機能ルートを遅延ロード
export const routes: Routes = [
{
path: "admin",
loadChildren: () =>
import("./admin/admin.routes").then((m) => m.ADMIN_ROUTES),
},
{
path: "dashboard",
loadComponent: () =>
import("./dashboard/dashboard.component").then(
(m) => m.DashboardComponent,
),
},
];
// 間違い - すべてを先読みロード
import { AdminModule } from "./admin/admin.module";
export const routes: Routes = [
{ path: "admin", component: AdminComponent }, // メインバンドルに含まれる
];
重いコンポーネントに @defer を使用する
<!-- 正解 - 重いコンポーネントはオンデマンドでロード -->
@defer (on viewport) {
<app-analytics-chart [data]="data()" />
} @placeholder {
<div class="chart-skeleton"></div>
}
<!-- 間違い - 重いコンポーネントが初期バンドルに含まれる -->
<app-analytics-chart [data]="data()" />
バレルファイルの再エクスポートを避ける
// 間違い - バレル全体をインポート、ツリーシェイキングが破損
import { Button, Modal, Table } from "@shared/components";
// 正解 - 直接インポート
import { Button } from "@shared/components/button/button.component";
import { Modal } from "@shared/components/modal/modal.component";
サードパーティライブラリを動的インポートする
// 正解 - 重いライブラリをオンデマンドでロード
async loadChart() {
const { Chart } = await import('chart.js');
this.chart = new Chart(this.canvas, config);
}
// 間違い - Chart.js をメインチャンクにバンドル
import { Chart } from 'chart.js';
4. レンダリングパフォーマンス (高)
@for で常に trackBy を使用する
<!-- 正解 - 効率的な DOM 更新 -->
@for (item of items(); track item.id) {
<app-item-card [item]="item" />
}
<!-- 間違い - 何か変更があると全リストが再レンダリング -->
@for (item of items(); track $index) {
<app-item-card [item]="item" />
}
大規模リストに仮想スクロールを使用する
import { CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll } from '@angular/cdk/scrolling';
@Component({
imports: [CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll],
template: `
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
<div *cdkVirtualFor="let item of items" class="item">
{{ item.name }}
</div>
</cdk-virtual-scroll-viewport>
`
})
メソッドではなく純粋パイプを推奨する
// 正解 - 純粋パイプ、メモ化される
@Pipe({ name: 'filterActive', standalone: true, pure: true })
export class FilterActivePipe implements PipeTransform {
transform(items: Item[]): Item[] {
return items.filter(i => i.active);
}
}
// テンプレート
@for (item of items() | filterActive; track item.id) { ... }
// 間違い - メソッドは変更検出毎に呼ばれる
@for (item of getActiveItems(); track item.id) { ... }
派生データに computed() を使用する
// 正解 - Computed、依存関係が変更されるまでキャッシュ
export class ProductStore {
products = signal<Product[]>([]);
filter = signal('');
filteredProducts = computed(() => {
const f = this.filter().toLowerCase();
return this.products().filter(p =>
p.name.toLowerCase().includes(f)
);
});
}
// 間違い - アクセスの度に再計算
get filteredProducts() {
return this.products.filter(p =>
p.name.toLowerCase().includes(this.filter)
);
}
5. サーバーサイドレンダリング (高)
インクリメンタルハイドレーションを設定する
// app.config.ts
import {
provideClientHydration,
withIncrementalHydration,
} from "@angular/platform-browser";
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(withIncrementalHydration(), withEventReplay()),
],
};
非重要なコンテンツを defer する
<!-- ファーストビューの重要なコンテンツ -->
<app-header />
<app-hero />
<!-- ビロー・ザ・フォールドは defer でハイドレーション -->
@defer (hydrate on viewport) {
<app-product-grid />
} @defer (hydrate on interaction) {
<app-chat-widget />
}
SSR データに TransferState を使用する
@Injectable({ providedIn: "root" })
export class DataService {
private http = inject(HttpClient);
private transferState = inject(TransferState);
private platformId = inject(PLATFORM_ID);
getData(key: string): Observable<Data> {
const stateKey = makeStateKey<Data>(key);
if (isPlatformBrowser(this.platformId)) {
const cached = this.transferState.get(stateKey, null);
if (cached) {
this.transferState.remove(stateKey);
return of(cached);
}
}
return this.http.get<Data>(`/api/${key}`).pipe(
tap((data) => {
if (isPlatformServer(this.platformId)) {
this.transferState.set(stateKey, data);
}
}),
);
}
}
6. テンプレート最適化 (中)
新しい制御フロー構文を使用する
<!-- 正解 - 新しい制御フロー (速い、バンドル小さい) -->
@if (user()) {
<span>{{ user()!.name }}</span>
} @else {
<span>Guest</span>
} @for (item of items(); track item.id) {
<app-item [item]="item" />
} @empty {
<p>No items</p>
}
<!-- 間違い - 従来の構造ディレクティブ -->
<span *ngIf="user; else guest">{{ user.name }}</span>
<ng-template #guest><span>Guest</span></ng-template>
複雑なテンプレート式を避ける
// 正解 - コンポーネントで事前計算
class Component {
items = signal<Item[]>([]);
sortedItems = computed(() =>
[...this.items()].sort((a, b) => a.name.localeCompare(b.name))
);
}
// テンプレート
@for (item of sortedItems(); track item.id) { ... }
// 間違い - テンプレートでのソート、レンダリングの度に実行
@for (item of items() | sort:'name'; track item.id) { ... }
7. 状態管理 (中)
セレクタを使用して再レンダリングを防ぐ
// 正解 - 選別的なサブスクリプション
@Component({
template: `<span>{{ userName() }}</span>`,
})
class HeaderComponent {
private store = inject(Store);
// userName が変更されたときのみ再レンダリング
userName = this.store.selectSignal(selectUserName);
}
// 間違い - 全状態をサブスクリプション
@Component({
template: `<span>{{ state().user.name }}</span>`,
})
class HeaderComponent {
private store = inject(Store);
// 状態が変更されたら再レンダリング
state = toSignal(this.store);
}
状態を機能と同じ場所に配置する
// 正解 - 機能スコープのストア
@Injectable() // NOT providedIn: 'root'
export class ProductStore { ... }
@Component({
providers: [ProductStore], // コンポーネントツリーにスコープ
})
export class ProductPageComponent {
store = inject(ProductStore);
}
// 間違い - すべてをグローバルストアに
@Injectable({ providedIn: 'root' })
export class GlobalStore {
// すべてのアプリ状態を含む - ツリーシェイキング困難
}
8. メモリ管理 (低~中)
サブスクリプションに takeUntilDestroyed を使用する
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({...})
export class DataComponent {
private destroyRef = inject(DestroyRef);
constructor() {
this.data$.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(data => this.process(data));
}
}
// 間違い - 手動でサブスクリプション管理
export class DataComponent implements OnDestroy {
private subscription!: Subscription;
ngOnInit() {
this.subscription = this.data$.subscribe(...);
}
ngOnDestroy() {
this.subscription.unsubscribe(); // 忘れやすい
}
}
サブスクリプションより Signal を推奨する
// 正解 - サブスクリプション不要
@Component({
template: `<div>{{ data().name }}</div>`,
})
export class Component {
data = toSignal(this.service.data$, { initialValue: null });
}
// 間違い - 手動でサブスクリプション
@Component({
template: `<div>{{ data?.name }}</div>`,
})
export class Component implements OnInit, OnDestroy {
data: Data | null = null;
private sub!: Subscription;
ngOnInit() {
this.sub = this.service.data$.subscribe((d) => (this.data = d));
}
ngOnDestroy() {
this.sub.unsubscribe();
}
}
クイックリファレンスチェックリスト
新規コンポーネント
-
changeDetection: ChangeDetectionStrategy.OnPush -
standalone: true - 状態に Signal を使用 (
signal()、input()、output()) - 依存関係に
inject()を使用 -
@forにtrack式を使用
パフォーマンスレビュー
- テンプレートにメソッドなし (パイプまたは computed を使用)
- 大規模リストを仮想化
- 重いコンポーネントを defer
- ルートを遅延ロード
- サードパーティライブラリを動的インポート
SSR チェック
- ハイドレーション設定済み
- 重要なコンテンツが最初にレンダリング
- 非重要なコンテンツは
@defer (hydrate on ...)を使用 - サーバー取得データに TransferState を使用
リソース
制限事項
- このスキルは、タスクが上記で説明されたスコープと明確に一致する場合にのみ使用してください。
- 出力を環境固有の検証、テスト、または専門家によるレビューの代替と見なさないでください。
- 必要な入力、権限、安全性の境界、または成功基準が不足している場合は、停止して説明を求めてください。
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- sickn33
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/sickn33/antigravity-awesome-skills / ライセンス: MIT
関連スキル
superfluid
Superfluidプロトコルおよびそのエコシステムに関するナレッジベースです。Superfluidについて情報を検索する際は、ウェブ検索の前にこちらを参照してください。対応キーワード:Superfluid、CFA、GDA、Super App、Super Token、stream、flow rate、real-time balance、pool(member/distributor)、IDA、sentinels、liquidation、TOGA、@sfpro/sdk、semantic money、yellowpaper、whitepaper
civ-finish-quotes
実質的なタスクが真に完了した際に、文明風の儀式的な引用句を追加します。ユーザーやエージェントが機能追加、リファクタリング、分析、設計ドキュメント、プロセス改善、レポート、執筆タスクといった実際の成果物を完成させるときに、明示的な依頼がなくても使用します。短い返信や小さな修正、未完成の作業には適用しません。
nookplot
Base(Ethereum L2)上のAIエージェント向け分散型調整ネットワークです。エージェントがオンチェーンアイデンティティを登録する、コンテンツを公開する、他のエージェントにメッセージを送る、マーケットプレイスで専門家を雇う、バウンティを投稿・請求する、レピュテーションを構築する、共有プロジェクトで協業する、リサーチチャレンジを解くことでNOOKをマイニングする、キュレーションされたナレッジを備えたスタンドアロンオンチェーンエージェントをデプロイする、またはアグリーメントとリワードで収益を得る場合に利用できます。エージェントネットワーク、エージェント調整、分散型エージェント、NOOKトークン、マイニングチャレンジ、ナレッジバンドル、エージェントレピュテーション、エージェントマーケットプレイス、ERC-2771メタトランザクション、Prepare-Sign-Relay、AgentFactory、またはNookplotが言及された場合にトリガーされます。
web3-polymarket
Polygon上でのPolymarket予測市場取引統合です。認証機能(L1 EIP-712、L2 HMAC-SHA256、ビルダーヘッダー)、注文発注(GTC/GTD/FOK/FAK、バッチ、ポストオンリー、ハートビート)、市場データ(Gamma API、Data API、オーダーブック、サブグラフ)、WebSocketストリーミング(市場・ユーザー・スポーツチャネル)、CTF操作(分割、統合、償却、ネガティブリスク)、ブリッジ機能(入金、出金、マルチチェーン)、およびガスレスリレイトランザクションに対応しています。AIエージェント、自動マーケットメーカー、予測市場UI、またはPolygraph上のPolymarketと統合するアプリケーション構築時に活用できます。
ethskills
Ethereum、EVM、またはブロックチェーン関連のリクエストに対応します。スマートコントラクト、dApps、ウォレット、DeFiプロトコルの構築、監査、デプロイ、インタラクションに適用されます。Solidityの開発、コントラクトアドレス、トークン規格(ERC-20、ERC-721、ERC-4626など)、Layer 2ネットワーク(Base、Arbitrum、Optimism、zkSync、Polygon)、Uniswap、Aave、Curveなどのプロトコルとの統合をカバーします。ガスコスト、コントラクトのデシマル設定、オラクルセキュリティ、リエントランシー、MEV、ブリッジング、ウォレット管理、オンチェーンデータの取得、本番環境へのデプロイ、プロトコル進化(EIPライフサイクル、フォーク追跡、今後の変更予定)といったトピックを含みます。
xxyy-trade
このスキルは、ユーザーが「トークン購入」「トークン売却」「トークンスワップ」「暗号資産取引」「取引ステータス確認」「トランザクション照会」「トークンスキャン」「フィード」「チェーン監視」「トークン照会」「トークン詳細」「トークン安全性確認」「ウォレット一覧表示」「マイウォレット」「AIスキャン」「自動スキャン」「ツイートスキャン」「オンボーディング」「IP確認」「IPホワイトリスト」「トークン発行」「自動売却」「損切り」「利益確定」「トレーリングストップ」「保有者」「トップホルダー」「KOLホルダー」などをリクエストした場合、またはSolana/ETH/BSC/BaseチェーンでXXYYを経由した取引について言及した場合に使用します。XXYY Open APIを通じてオンチェーン取引とデータ照会を実現します。