mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-07-30 03:09:50 +08:00
Compare commits
5 Commits
8f6e86b240
...
da93e66778
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da93e66778 | ||
|
|
c2428c6ff0 | ||
|
|
9c22002f57 | ||
|
|
a791b8b262 | ||
|
|
da0a822367 |
@ -1,4 +0,0 @@
|
||||
## Docs Only
|
||||
|
||||
This repo contains docs only; No code is allowed and the content is not
|
||||
licensed under any Open Source license.
|
||||
421
manifest.json
421
manifest.json
@ -1,421 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"generatedAt": "2026-02-17T21:01:05.349Z",
|
||||
"branch": "main",
|
||||
"prompts": [
|
||||
{
|
||||
"name": "PricingService Invocable Scaffold",
|
||||
"path": "prompts/apex-development/scaffold-invocable-apex.md",
|
||||
"description": "Create a PricingService Apex class with an invocable getPricing method and typed inputs/outputs for Flow.",
|
||||
"category": "apex-development",
|
||||
"tags": [
|
||||
"apex",
|
||||
"invocable",
|
||||
"flow",
|
||||
"scaffold"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Travel Plan Object Scaffold",
|
||||
"path": "prompts/apex-development/scaffold-object.md",
|
||||
"description": "Provision a Travel Plan custom object with fields, layout, tab, and permission set ready for deployment.",
|
||||
"category": "apex-development",
|
||||
"tags": [
|
||||
"salesforce",
|
||||
"custom-object",
|
||||
"scaffold",
|
||||
"deployment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Trigger Refactor Helper",
|
||||
"path": "prompts/apex-development/trigger-refactoring.md",
|
||||
"description": "Refactor the Opportunity trigger into a handler pattern with tests",
|
||||
"category": "apex-development",
|
||||
"tags": [
|
||||
"apex",
|
||||
"refactor",
|
||||
"testing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Account Test Data Generator",
|
||||
"path": "prompts/data-operations/create-fake-data.md",
|
||||
"description": "Create an Apex script that generates 10 realistic Account records with complete business data including name, account number, type, industry, annual revenue, ticker symbol, and number of employees. Includes documentation on how to execute the script anonymously.",
|
||||
"category": "data-operations",
|
||||
"tags": [
|
||||
"salesforce",
|
||||
"apex",
|
||||
"data-generation",
|
||||
"test-data",
|
||||
"developer",
|
||||
"admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Account Layout Field Enhancement",
|
||||
"path": "prompts/getting-started/add-fields-pagelayout.md",
|
||||
"description": "Add Type and Industry fields to the Account page layout for better data organization and reporting.",
|
||||
"category": "getting-started",
|
||||
"tags": [
|
||||
"salesforce",
|
||||
"account",
|
||||
"page-layout",
|
||||
"admin",
|
||||
"data-management"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Lead Source Picklist Enhancement",
|
||||
"path": "prompts/getting-started/add-picklist-value.md",
|
||||
"description": "Add a new 'Referral' value to the Lead Source picklist on the Lead object for better lead tracking.",
|
||||
"category": "getting-started",
|
||||
"tags": [
|
||||
"salesforce",
|
||||
"lead",
|
||||
"picklist",
|
||||
"data-management",
|
||||
"admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "High Priority Case Assignment Rule",
|
||||
"path": "prompts/getting-started/create-assignment-rules.md",
|
||||
"description": "Create an assignment rule that automatically routes high priority Cases to a specific queue or user for immediate attention.",
|
||||
"category": "getting-started",
|
||||
"tags": [
|
||||
"salesforce",
|
||||
"case-management",
|
||||
"assignment-rules",
|
||||
"automation",
|
||||
"admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Project Object Quickstart",
|
||||
"path": "prompts/getting-started/create-basic-custom-object.md",
|
||||
"description": "Scaffold a Project__c custom object with core fields and deployable metadata.",
|
||||
"category": "getting-started",
|
||||
"tags": [
|
||||
"salesforce",
|
||||
"custom-object",
|
||||
"beginner"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Opportunity Days Open Formula",
|
||||
"path": "prompts/getting-started/create-opp-formula.md",
|
||||
"description": "Create a formula field on Opportunity that calculates the number of days since the record was created.",
|
||||
"category": "getting-started",
|
||||
"tags": [
|
||||
"salesforce",
|
||||
"opportunity",
|
||||
"formula-field",
|
||||
"automation",
|
||||
"admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Account Editor Permission Set",
|
||||
"path": "prompts/getting-started/create-permission-set.md",
|
||||
"description": "Create a permission set that grants edit access to Account records and assign it to the current user.",
|
||||
"category": "getting-started",
|
||||
"tags": [
|
||||
"salesforce",
|
||||
"permission-set",
|
||||
"security",
|
||||
"admin",
|
||||
"user-management"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Account Opportunity Count Roll-Up",
|
||||
"path": "prompts/getting-started/create-rollup-summary.md",
|
||||
"description": "Create a roll-up summary field on Account that counts related Opportunities for better relationship tracking.",
|
||||
"category": "getting-started",
|
||||
"tags": [
|
||||
"salesforce",
|
||||
"account",
|
||||
"opportunity",
|
||||
"rollup-summary",
|
||||
"automation",
|
||||
"admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Hello World LWC Starter",
|
||||
"path": "prompts/getting-started/first-lightning-web-component.md",
|
||||
"description": "Generate a greeting Lightning Web Component and explain its HTML, JS, and metadata files.",
|
||||
"category": "getting-started",
|
||||
"tags": [
|
||||
"lwc",
|
||||
"ui",
|
||||
"beginner"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Opportunity Days Till Close Formula",
|
||||
"path": "prompts/getting-started/formula-field-days-till-close.md",
|
||||
"description": "Build an Opportunity formula that calculates days between CreatedDate and CloseDate.",
|
||||
"category": "getting-started",
|
||||
"tags": [
|
||||
"formula",
|
||||
"opportunity",
|
||||
"admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Opportunity Close Date Validation",
|
||||
"path": "prompts/getting-started/opp-validation-rule.md",
|
||||
"description": "Create a validation rule on Opportunity that prevents saving when the Close Date is in the past.",
|
||||
"category": "getting-started",
|
||||
"tags": [
|
||||
"salesforce",
|
||||
"opportunity",
|
||||
"validation-rule",
|
||||
"data-quality",
|
||||
"admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "StringUtils Utility Scaffold",
|
||||
"path": "prompts/getting-started/reusable-apex-string-utility-class.md",
|
||||
"description": "Create a foundational StringUtils Apex helper with core string methods and unit tests.",
|
||||
"category": "getting-started",
|
||||
"tags": [
|
||||
"apex",
|
||||
"utility",
|
||||
"testing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "DateUtils Utility Primer",
|
||||
"path": "prompts/getting-started/simple-apex-utility-class.md",
|
||||
"description": "Generate a documented DateUtils Apex class with daysBetween and addDays helpers.",
|
||||
"category": "getting-started",
|
||||
"tags": [
|
||||
"apex",
|
||||
"utility",
|
||||
"beginner"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Opportunity Stage Score Formula",
|
||||
"path": "prompts/getting-started/stage-based-opportunity-scoring-formula.md",
|
||||
"description": "Craft a readable Opportunity formula that assigns numeric scores per stage.",
|
||||
"category": "getting-started",
|
||||
"tags": [
|
||||
"formula",
|
||||
"opportunity",
|
||||
"admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Test Data Factory Kickoff",
|
||||
"path": "prompts/getting-started/test-data-factory-basics.md",
|
||||
"description": "Build an Apex factory for Accounts, Contacts, and Opportunities with usage examples.",
|
||||
"category": "getting-started",
|
||||
"tags": [
|
||||
"apex",
|
||||
"testing",
|
||||
"test-data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Account Trigger Starter",
|
||||
"path": "prompts/getting-started/trigger-and-handler-example.md",
|
||||
"description": "Create a simple Account trigger and handler that sets default values before insert.",
|
||||
"category": "getting-started",
|
||||
"tags": [
|
||||
"apex",
|
||||
"trigger",
|
||||
"beginner"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Account Employee Validation Rule",
|
||||
"path": "prompts/getting-started/validation-rule-on-account.md",
|
||||
"description": "Draft a rule that blocks Account save when NumberOfEmployees is blank.",
|
||||
"category": "getting-started",
|
||||
"tags": [
|
||||
"validation-rule",
|
||||
"account",
|
||||
"admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Opportunity Approval Investigation",
|
||||
"path": "prompts/investigation-triage/fix-complex-approval-logic.md",
|
||||
"description": "Diagnose inconsistent approval checkbox behavior on Opportunities and propose a remediation plan.",
|
||||
"category": "investigation-triage",
|
||||
"tags": [
|
||||
"investigation",
|
||||
"triage",
|
||||
"opportunity",
|
||||
"approval-process"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Asset Tracker Spec Build",
|
||||
"path": "prompts/spec-driven-dev/asset-tracker.md",
|
||||
"description": "Gather requirements and deliver a full Asset tracker app via a spec-driven workflow.",
|
||||
"category": "spec-driven-dev",
|
||||
"tags": [
|
||||
"spec-driven",
|
||||
"lwc",
|
||||
"custom-object",
|
||||
"app"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Apex Unit Test Generator",
|
||||
"path": "prompts/testing-automation/create-unit-test.md",
|
||||
"description": "Create a comprehensive test class for any Apex class with test data setup and result verification.",
|
||||
"category": "testing-automation",
|
||||
"tags": [
|
||||
"salesforce",
|
||||
"apex",
|
||||
"testing",
|
||||
"unit-test",
|
||||
"automation",
|
||||
"developer"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Run All Apex Tests",
|
||||
"path": "prompts/testing-automation/run-all-test.md",
|
||||
"description": "Execute all Apex tests in the org and display a comprehensive summary including total tests run, number passed, and overall code coverage percentage.",
|
||||
"category": "testing-automation",
|
||||
"tags": [
|
||||
"salesforce",
|
||||
"apex",
|
||||
"testing",
|
||||
"automation",
|
||||
"test-coverage"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Compliment Mixer App",
|
||||
"path": "prompts/vibe-coding/compliment-mixer.md",
|
||||
"description": "Build a playful compliment generator with custom UI, persistence, and recent history.",
|
||||
"category": "vibe-coding",
|
||||
"tags": [
|
||||
"vibe-coding",
|
||||
"lwc",
|
||||
"app",
|
||||
"fun"
|
||||
]
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"name": "Apex Development Guardrails",
|
||||
"path": "rules/apex-development/apex-requirements.md",
|
||||
"description": "Enforce best practices for Apex architecture, performance, security, and testing.",
|
||||
"category": "apex-development",
|
||||
"tags": [
|
||||
"apex",
|
||||
"rules",
|
||||
"guardrails",
|
||||
"testing",
|
||||
"security"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "LWC Development Guardrails",
|
||||
"path": "rules/lwc-development/lwc-requirements.md",
|
||||
"description": "Standardize Lightning Web Component architecture, UX, and tooling practices.",
|
||||
"category": "lwc-development",
|
||||
"tags": [
|
||||
"lwc",
|
||||
"rules",
|
||||
"ux",
|
||||
"architecture"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Spec-Driven App Development Standards",
|
||||
"path": "rules/spec-driven-dev/app-dev.md",
|
||||
"description": "Apply architecture, integration, and governance requirements for specification-led Salesforce builds.",
|
||||
"category": "spec-driven-dev",
|
||||
"tags": [
|
||||
"spec-driven",
|
||||
"architecture",
|
||||
"governance",
|
||||
"rules"
|
||||
]
|
||||
}
|
||||
],
|
||||
"skills": [
|
||||
{
|
||||
"name": "apex-class",
|
||||
"path": "skills/apex-development/apex-class/SKILL.md",
|
||||
"folderPath": "skills/apex-development/apex-class",
|
||||
"category": "apex-development",
|
||||
"files": [
|
||||
"SKILL.md",
|
||||
"examples/AccountDeduplicationBatch.cls",
|
||||
"examples/AccountSelector.cls",
|
||||
"examples/AccountService.cls",
|
||||
"templates/abstract.cls",
|
||||
"templates/batch.cls",
|
||||
"templates/domain.cls",
|
||||
"templates/dto.cls",
|
||||
"templates/exception.cls",
|
||||
"templates/interface.cls",
|
||||
"templates/queueable.cls",
|
||||
"templates/schedulable.cls",
|
||||
"templates/selector.cls",
|
||||
"templates/service.cls",
|
||||
"templates/utility.cls"
|
||||
],
|
||||
"description": "Generate production-ready Apex classes for Salesforce following enterprise best practices. Covers service classes, selectors, domain classes, batch/queueable/schedulable, DTOs, utilities, interfaces, abstract classes, and custom exceptions. Not for triggers or unit tests."
|
||||
},
|
||||
{
|
||||
"name": "trigger-refactor-pipeline",
|
||||
"path": "skills/apex-development/trigger-refactor-pipeline/SKILL.md",
|
||||
"folderPath": "skills/apex-development/trigger-refactor-pipeline",
|
||||
"category": "apex-development",
|
||||
"files": [
|
||||
"SKILL.md",
|
||||
"assets/test_template.apex",
|
||||
"references/handler_patterns.md",
|
||||
"scripts/analyze_trigger.py"
|
||||
],
|
||||
"description": "Refactor Salesforce triggers into handler patterns with automated test generation and deployment. Use when modernizing legacy triggers with DML/SOQL in loops or inconsistent patterns.",
|
||||
"license": "Apache-2.0",
|
||||
"compatibility": "Requires Salesforce CLI, Python 3.9+",
|
||||
"allowedTools": "Bash Read Write"
|
||||
},
|
||||
{
|
||||
"name": "deployment-readiness-check",
|
||||
"path": "skills/metadata-deployments/deployment-readiness-check/SKILL.md",
|
||||
"folderPath": "skills/metadata-deployments/deployment-readiness-check",
|
||||
"category": "metadata-deployments",
|
||||
"files": [
|
||||
"SKILL.md",
|
||||
"assets/deployment_checklist.md",
|
||||
"references/rollback_procedures.md",
|
||||
"scripts/check_metadata.sh"
|
||||
],
|
||||
"description": "Comprehensive pre-deployment validation checklist for Salesforce releases. Use before deploying to production to catch metadata issues, test coverage gaps, and configuration errors.",
|
||||
"license": "Apache-2.0",
|
||||
"compatibility": "Requires Salesforce CLI, jq, bash",
|
||||
"allowedTools": "Bash Read"
|
||||
},
|
||||
{
|
||||
"name": "apex-test-class",
|
||||
"path": "skills/testing-automation/apex-test-class/SKILL.md",
|
||||
"folderPath": "skills/testing-automation/apex-test-class",
|
||||
"category": "testing-automation",
|
||||
"files": [
|
||||
"SKILL.md",
|
||||
"references/assertion-patterns.md",
|
||||
"references/async-testing.md",
|
||||
"references/mocking-patterns.md",
|
||||
"references/test-data-factory.md"
|
||||
],
|
||||
"description": "Apex test class generation with TestDataFactory patterns, bulk testing (200+ records), mocking strategies for callouts and async operations, and assertion best practices. Use when creating new Apex test classes, improving test coverage, refactoring existing tests, or implementing proper testing patterns for triggers, services, controllers, batch jobs, queueables, and integrations."
|
||||
}
|
||||
]
|
||||
}
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@salesforce/afv-skills",
|
||||
"version": "1.13.0",
|
||||
"version": "1.27.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@salesforce/afv-skills",
|
||||
"version": "1.13.0",
|
||||
"version": "1.27.0",
|
||||
"license": "CC-BY-NC-4.0",
|
||||
"devDependencies": {
|
||||
"@salesforce/ui-bundle-template-app-react-sample-b2e": "^10.2.2",
|
||||
|
||||
68
skills/experience-ui-bundle-performance-optimize/README.md
Normal file
68
skills/experience-ui-bundle-performance-optimize/README.md
Normal file
@ -0,0 +1,68 @@
|
||||
# experience-ui-bundle-performance-optimize
|
||||
|
||||
Frontend performance auditing and optimization skill for Salesforce React UI Bundles. Measures Vite build output against size budgets, then applies route-level code splitting and vendor chunking to cut first-load JavaScript — without touching data layer, routing structure, or visual design.
|
||||
|
||||
## Features
|
||||
|
||||
- **Bundle Measurement**: Reads Vite's build report (or runs the bundled analyzer) against explicit size budgets for entry chunks, individual chunks, route chunks, and CSS
|
||||
- **Route-Level Code Splitting**: Converts eager page imports in `routes.tsx` to `React.lazy`, so each page ships its own chunk fetched on first navigation
|
||||
- **Vendor Chunking**: Configures `manualChunks` in `vite.config.ts` to separate rarely-changing dependencies (React, router, UI primitives, charts) into their own cacheable chunks
|
||||
- **Render-Waste Diagnosis**: Guidance for `memo`/`useMemo`/debouncing/virtualization — applied only against a reported symptom, not speculatively
|
||||
- **Verification Workflow**: Rebuild, re-measure, and click through every navigation entry to confirm lazy chunks load correctly
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Invoke the skill
|
||||
|
||||
```
|
||||
Skill: experience-ui-bundle-performance-optimize
|
||||
Request: "This UI bundle's build is throwing a chunk-size warning, can you fix it?"
|
||||
```
|
||||
|
||||
The skill also self-activates when a build prints Vite's `"Some chunks are larger than 500 kB"` warning, or when the user mentions slow load, bundle size, code splitting, or Core Web Vitals for a project under `uiBundles/*/src/`.
|
||||
|
||||
### 2. Run the analyzer directly
|
||||
|
||||
```bash
|
||||
node skills/experience-ui-bundle-performance-optimize/scripts/analyze-bundle.mjs <app-dir>/dist
|
||||
```
|
||||
|
||||
Add `--json` for machine-readable output. Exits `0` when every budget passes, `1` when any budget is exceeded (suitable for CI gating), `2` on usage/IO errors.
|
||||
|
||||
### 3. Typical use cases
|
||||
|
||||
- Diagnose why a UI bundle's `npm run build` prints a chunk-size warning
|
||||
- Reduce first-load JavaScript before deploying a UI bundle to an org
|
||||
- Add code splitting to a UI bundle app that has grown past its initial page set
|
||||
- Investigate reported UI sluggishness (typing lag, slow list scroll, tab-switch delay)
|
||||
|
||||
## Results on the Reference Sample
|
||||
|
||||
Applied to `samples/ui-bundle-template-app-react-sample-b2e` (Property Management app):
|
||||
|
||||
| | Before | After |
|
||||
|---|---|---|
|
||||
| Entry chunk | 1,104.74 kB (326.44 kB gzip) | 50.21 kB (16.28 kB gzip) |
|
||||
| Route chunks | none — all pages eager | 4 chunks, 9–16 kB each, loaded on navigation |
|
||||
| Vendor chunks | none | `react`, `router`, `radix`, `charts` — cacheable across deploys |
|
||||
| Vite size warning | yes | no |
|
||||
|
||||
Verified at runtime: the home route renders correctly, and navigating to each page fetches exactly that page's lazy chunk (confirmed via network trace), nothing more.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [SKILL.md](SKILL.md) — full workflow: measure → split routes → split vendors → fix render waste → verify, with size budgets and template-specific rules
|
||||
- [scripts/analyze-bundle.mjs](scripts/analyze-bundle.mjs) — standalone budget checker for `dist/` output
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Does not touch the GraphQL data layer, router library, or component library — that is a rewrite, not an optimization
|
||||
- Does not enable production sourcemaps to "measure" — uses the build report or the analyzer instead
|
||||
- Does not change `outDir`, `assetsDir`, or `base` in `vite.config.ts` — those are relied on by deployment
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `experience-ui-bundle-frontend-generate` — for visual/styling changes to an existing UI bundle app
|
||||
- `experience-ui-bundle-app-coordinate` — for building a new UI bundle app from scratch
|
||||
- `experience-ui-bundle-deploy` — for deploying the optimized bundle to a Salesforce org
|
||||
- `experience-ui-bundle-salesforce-data-access` — for GraphQL/data-layer concerns (org latency, query shape) that this skill does not address
|
||||
135
skills/experience-ui-bundle-performance-optimize/SKILL.md
Normal file
135
skills/experience-ui-bundle-performance-optimize/SKILL.md
Normal file
@ -0,0 +1,135 @@
|
||||
---
|
||||
name: experience-ui-bundle-performance-optimize
|
||||
description: "MUST activate when a UI bundle build prints chunk-size warnings (\"Some chunks are larger than 500 kB\"), when the user mentions slow load, bundle size, code splitting, lazy loading, re-render, or Core Web Vitals for an app under uiBundles/*/src/, or when auditing frontend performance of a React UI bundle. Use this skill to measure bundle output, split oversized chunks, lazy-load routes, and fix render waste. Do NOT use for visual/styling changes (use experience-ui-bundle-frontend-generate), for creating new apps (use experience-ui-bundle-app-coordinate), or for Apex/server performance (use platform-apex-logs-debug)."
|
||||
compatibility: UI Bundle React template (Vite 5+, React 18/19, react-router 7)
|
||||
metadata:
|
||||
version: "1.0"
|
||||
relatedSkills: experience-ui-bundle-frontend-generate, experience-ui-bundle-app-coordinate, experience-ui-bundle-deploy
|
||||
---
|
||||
|
||||
# UI Bundle Performance Optimization
|
||||
|
||||
Measure first, then optimize. Every change in this skill is driven by build output and verified by rebuilding — never apply techniques speculatively.
|
||||
|
||||
UI bundles run inside the Salesforce platform, so first-load JavaScript is the dominant cost: the bundle is fetched through the org's CDN/proxy and shares the page with platform chrome. A default template app that imports charts, Radix primitives, and all pages eagerly ships a single ~1.1 MB entry chunk (~326 kB gzip). The workflow below reduced that entry to ~50 kB (16 kB gzip) in the sample Property Management app with two localized changes.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Measure** — build and read the chunk report (or run `scripts/analyze-bundle.mjs`).
|
||||
2. **Split routes** — lazy-load every non-index page with `React.lazy`.
|
||||
3. **Split vendors** — separate rarely-changing dependencies with `manualChunks`.
|
||||
4. **Fix render waste** — only if the user reports interaction jank, not by default.
|
||||
5. **Verify** — rebuild, compare the report, and click through the app in dev/preview.
|
||||
|
||||
---
|
||||
|
||||
## 1. Measure the Current Bundle
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Read the `dist/assets/*.js` lines Vite prints. Or run the bundled analyzer for budgets and JSON output:
|
||||
|
||||
```bash
|
||||
node <skill-dir>/scripts/analyze-bundle.mjs <app-dir>/dist
|
||||
```
|
||||
|
||||
Budgets (raw, minified — not gzip):
|
||||
|
||||
| Asset | Budget | Action when exceeded |
|
||||
|---|---|---|
|
||||
| Entry chunk (`index-*.js`) | ≤ 300 kB | Split routes (§2), then vendors (§3) |
|
||||
| Any single chunk | ≤ 500 kB | Split that chunk's largest dependency |
|
||||
| Route (lazy) chunk | ≤ 100 kB | Move heavy deps inside the page behind `lazy` |
|
||||
| CSS total | ≤ 150 kB | Check Tailwind content globs; purge unused |
|
||||
|
||||
If the entry chunk is already within budget, stop — report the numbers and make no changes.
|
||||
|
||||
## 2. Route-Level Code Splitting
|
||||
|
||||
`routes.tsx` is the single routing source of truth in this template. Convert every page except the index route and `NotFound` to `React.lazy`:
|
||||
|
||||
```tsx
|
||||
import { lazy, Suspense } from 'react';
|
||||
import AppLayout from './appLayout';
|
||||
import Home from './pages/Home'; // eager: first paint
|
||||
import NotFound from './pages/NotFound'; // eager: tiny, avoids flash on bad URLs
|
||||
|
||||
const PropertySearch = lazy(() => import('./pages/PropertySearch'));
|
||||
|
||||
const suspend = (el: React.ReactNode) => <Suspense fallback={null}>{el}</Suspense>;
|
||||
|
||||
// in the RouteObject tree:
|
||||
{ path: 'properties', element: suspend(<PropertySearch />), handle: { showInNavigation: true, label: 'Properties' } }
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Keep the index route eager — lazy-loading it delays first paint instead of improving it.
|
||||
- Keep `handle` metadata exactly as-is; navigation in `appLayout.tsx` is generated from it.
|
||||
- Use `fallback={null}` or a lightweight skeleton — never a spinner that flashes on fast networks.
|
||||
- Do not lazy-load `appLayout.tsx`; it wraps every route.
|
||||
- Each page becomes its own chunk (typically 9–16 kB in this template) fetched on first navigation.
|
||||
|
||||
## 3. Vendor Chunking with `manualChunks`
|
||||
|
||||
In `vite.config.ts`, add `rollupOptions.output.manualChunks` inside the existing `build` block. Match specific packages before generic ones:
|
||||
|
||||
```ts
|
||||
build: {
|
||||
// ...existing outDir/assetsDir/sourcemap...
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (!id.includes('node_modules')) return;
|
||||
if (id.includes('recharts') || id.includes('d3-')) return 'charts';
|
||||
if (id.includes('radix-ui')) return 'radix';
|
||||
if (id.includes('lucide-react')) return 'icons'; // BEFORE the react check
|
||||
if (id.includes('react-router')) return 'router';
|
||||
if (id.includes('react')) return 'react';
|
||||
return 'vendor';
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
**Pitfall — substring matching:** `id.includes('react')` also matches `lucide-react`, `react-day-picker`, and anything else with "react" in its path. Match the specific packages *first* (as above) or the react chunk silently absorbs them and stays oversized. After any `manualChunks` change, confirm chunk contents shifted the way you expected — sizes in the build report are the evidence.
|
||||
|
||||
Why vendor splitting matters even when total bytes stay the same: vendor chunks change rarely, so their hashed filenames survive app-code deploys and stay in the browser cache; and heavy optional deps (charts) stop blocking pages that never render a chart.
|
||||
|
||||
## 4. Render Waste (only when interaction is slow)
|
||||
|
||||
Apply only with a reported symptom (typing lag, slow list scroll, sluggish tab switch):
|
||||
|
||||
- Wrap list-row components in `React.memo` when a parent re-renders frequently with unchanged row props.
|
||||
- Hoist object/array literals and inline handlers out of JSX in hot paths (`useMemo`/`useCallback`) — but only where profiling shows re-renders, otherwise it is noise.
|
||||
- Paginate or virtualize lists over ~200 rows; this template already ships `PaginationControls` — prefer it over rendering full result sets.
|
||||
- Debounce search inputs that fire a GraphQL query per keystroke (300 ms is a good default).
|
||||
- Never fetch in a component body; keep data access in the existing hooks/api layer so React Query-style caching (or memoized hooks) prevents duplicate requests.
|
||||
|
||||
Diagnose with React DevTools Profiler (in dev mode) before changing code; state the suspected wasteful component and confirm it in the flame graph.
|
||||
|
||||
## 5. Verify
|
||||
|
||||
1. `npm run build` — compare against the §1 baseline. Expect: entry chunk sharply smaller, one chunk per lazy page, named vendor chunks, and no Vite size warning.
|
||||
2. Run the analyzer again — it exits non-zero if any budget is still exceeded.
|
||||
3. `npm run dev` (or `npm run preview` for the built output) and click every navigation entry. Watch the network panel: each first visit to a lazy route should fetch exactly that route's chunk, and revisits should fetch nothing.
|
||||
4. `npm test` if the project has tests; route conversion to `lazy` must not change rendered output.
|
||||
|
||||
Report to the user: before/after entry size, chunk count, and which techniques were applied. Example from the sample Property Management app:
|
||||
|
||||
| | Before | After (§2 + §3) |
|
||||
|---|---|---|
|
||||
| Entry chunk | 1,104.74 kB (326 kB gzip) | 50.21 kB (16.3 kB gzip) |
|
||||
| Route chunks | none (all eager) | 9–16 kB each, loaded on navigation |
|
||||
| Vendor chunks | none | react / router / radix / charts, cached across deploys |
|
||||
| Vite size warning | yes | no |
|
||||
|
||||
## Boundaries
|
||||
|
||||
- **Do not** enable `sourcemap: true` in production builds to "measure" — use the build report or the analyzer script.
|
||||
- **Do not** remove or rename the existing `outDir`/`assetsDir`/`base: './'` settings; deployment (`experience-ui-bundle-deploy`) depends on them.
|
||||
- **Do not** replace the GraphQL data layer, router, or component library in the name of performance — that is a rewrite, not an optimization; confirm scope with the user first.
|
||||
- Salesforce org latency (GraphQL 401s locally, API response times) is not addressable from the bundle; direct data-layer concerns to `experience-ui-bundle-salesforce-data-access`.
|
||||
@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* UI Bundle build-output analyzer.
|
||||
*
|
||||
* Reads a Vite dist/ directory and reports every JS/CSS asset against the
|
||||
* skill's size budgets, flagging what to split first.
|
||||
*
|
||||
* Usage: node analyze-bundle.mjs <path-to-dist> [--json]
|
||||
*
|
||||
* Exit code: 0 when all budgets pass, 1 when any budget is exceeded,
|
||||
* 2 on usage/IO errors — so it can gate CI.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const BUDGETS = {
|
||||
entryChunkKb: 300, // index-*.js
|
||||
anyChunkKb: 500,
|
||||
routeChunkKb: 100, // lazy page chunks
|
||||
cssTotalKb: 150,
|
||||
};
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const asJson = args.includes('--json');
|
||||
const distDir = args.find((a) => !a.startsWith('--'));
|
||||
|
||||
if (!distDir) {
|
||||
console.error('Usage: node analyze-bundle.mjs <path-to-dist> [--json]');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const assetsDir = path.join(distDir, 'assets');
|
||||
if (!fs.existsSync(assetsDir)) {
|
||||
console.error(`No assets directory at ${assetsDir} — run the app's \`npm run build\` first.`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const kb = (bytes) => Math.round((bytes / 1024) * 100) / 100;
|
||||
|
||||
const assets = fs
|
||||
.readdirSync(assetsDir)
|
||||
.filter((f) => f.endsWith('.js') || f.endsWith('.css'))
|
||||
.map((f) => {
|
||||
const size = fs.statSync(path.join(assetsDir, f)).size;
|
||||
const isJs = f.endsWith('.js');
|
||||
const isEntry = isJs && /^index-/.test(f);
|
||||
// Vite names lazy chunks after their source module (e.g. PropertySearch-<hash>.js)
|
||||
const isRoute = isJs && !isEntry && /^[A-Z]/.test(f);
|
||||
return { file: f, sizeKb: kb(size), type: isJs ? 'js' : 'css', isEntry, isRoute };
|
||||
})
|
||||
.sort((a, b) => b.sizeKb - a.sizeKb);
|
||||
|
||||
const findings = [];
|
||||
|
||||
for (const a of assets.filter((x) => x.type === 'js')) {
|
||||
if (a.isEntry && a.sizeKb > BUDGETS.entryChunkKb) {
|
||||
findings.push({
|
||||
severity: 'error',
|
||||
file: a.file,
|
||||
sizeKb: a.sizeKb,
|
||||
budgetKb: BUDGETS.entryChunkKb,
|
||||
message: 'Entry chunk over budget — apply route-level code splitting (SKILL.md §2), then vendor chunking (§3).',
|
||||
});
|
||||
} else if (a.sizeKb > BUDGETS.anyChunkKb) {
|
||||
findings.push({
|
||||
severity: 'error',
|
||||
file: a.file,
|
||||
sizeKb: a.sizeKb,
|
||||
budgetKb: BUDGETS.anyChunkKb,
|
||||
message: 'Chunk over budget — split its largest dependency into its own manualChunks entry (§3). Check for substring matches absorbing extra packages.',
|
||||
});
|
||||
} else if (a.isRoute && a.sizeKb > BUDGETS.routeChunkKb) {
|
||||
findings.push({
|
||||
severity: 'warning',
|
||||
file: a.file,
|
||||
sizeKb: a.sizeKb,
|
||||
budgetKb: BUDGETS.routeChunkKb,
|
||||
message: 'Route chunk is heavy — lazy-load this page\'s heavy dependencies or move shared code up.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const cssTotal = kb(assets.filter((a) => a.type === 'css').reduce((s, a) => s + a.sizeKb * 1024, 0));
|
||||
if (cssTotal > BUDGETS.cssTotalKb) {
|
||||
findings.push({
|
||||
severity: 'warning',
|
||||
file: '(all css)',
|
||||
sizeKb: cssTotal,
|
||||
budgetKb: BUDGETS.cssTotalKb,
|
||||
message: 'CSS total over budget — check Tailwind content globs for over-broad patterns.',
|
||||
});
|
||||
}
|
||||
|
||||
const hasVendorSplit = assets.some((a) => /^(react|router|vendor|charts|radix|icons)-/.test(a.file));
|
||||
const errors = findings.filter((f) => f.severity === 'error');
|
||||
|
||||
const result = {
|
||||
distDir,
|
||||
budgets: BUDGETS,
|
||||
vendorChunksDetected: hasVendorSplit,
|
||||
assets,
|
||||
findings,
|
||||
pass: errors.length === 0,
|
||||
};
|
||||
|
||||
if (asJson) {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} else {
|
||||
console.log(`Assets in ${assetsDir} (largest first):\n`);
|
||||
for (const a of assets) {
|
||||
const flag = findings.find((f) => f.file === a.file);
|
||||
const mark = flag ? (flag.severity === 'error' ? '✗' : '⚠') : '✓';
|
||||
console.log(` ${mark} ${a.sizeKb.toString().padStart(9)} kB ${a.file}`);
|
||||
}
|
||||
console.log(`\nVendor chunks detected: ${hasVendorSplit ? 'yes' : 'no (see SKILL.md §3)'}`);
|
||||
if (findings.length) {
|
||||
console.log(`\n${findings.length} finding(s):`);
|
||||
for (const f of findings) console.log(` [${f.severity}] ${f.file} (${f.sizeKb} kB > ${f.budgetKb} kB): ${f.message}`);
|
||||
} else {
|
||||
console.log('\nAll budgets pass.');
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(result.pass ? 0 : 1);
|
||||
Loading…
Reference in New Issue
Block a user