ADR-014: Index Page Performance Strategy
Status
Section titled “Status”Accepted
Note: This ADR documents the implementation of patterns defined in ADR 020: Page Performance Patterns. Refer to ADR 020 for comprehensive performance guidelines.
Context
Section titled “Context”The index.astro homepage uses several components and data structures that could impact performance. Key concerns raised:
- ExpandableFeatureCard JS: Does it introduce unnecessary hydration?
- Client directives: Are interactive components properly deferred?
- Lazy loading: Are images and iframes optimized?
- Hardcoded metrics: Should Lighthouse scores be dynamically generated?
This ADR documents the current performance status and optimization decisions.
Performance Analysis Results
Section titled “Performance Analysis Results”JavaScript Bundle Analysis
Section titled “JavaScript Bundle Analysis”Build output (production):
dist/_astro/page.CY1iZwUD.js: 2.07 kB │ gzip: 1.04 kBdist/_astro/ClientRouter.js: 15.12 kB │ gzip: 5.18 kB (View Transitions)ExpandableFeatureCard script:
- Type:
<script type="module">(NOTis:inline) - Size: ~1KB gzipped
- Behavior: Deferred module loading (non-blocking)
- Functionality: Syncs
<details>expand/collapse across feature cards
Total page JS (excluding View Transitions):
- Homepage-specific: ~1KB gzipped
- Zero Preact/React islands
- Zero
client:*directives
Current Implementation Status
Section titled “Current Implementation Status”✅ Zero-JS Baseline Maintained
- No client-side framework hydration
- No
client:load,client:visible, orclient:idledirectives - ExpandableFeatureCard uses native HTML
<details>element - Progressive enhancement via deferred module script
✅ Lazy Loading
- No images on index.astro (only SVG icons)
- No iframes present
- All images in other pages use Astro’s
<Image>component with automatic optimization
✅ CSS-First Interactivity
<details>element provides expand/collapse without JS- JS only enhances UX by syncing multiple cards
- Graceful degradation: works without JS
❌ Hardcoded Metrics
- Lighthouse scores manually defined in frontmatter
- Tech stack versions manually maintained
- No dynamic generation from build artifacts
Decision Drivers
Section titled “Decision Drivers”- Performance First: Maintain 95+ Lighthouse scores
- Zero-JS Baseline: Ship minimal JavaScript by default
- Progressive Enhancement: JS enhances, doesn’t enable
- Maintainability: Balance automation vs. simplicity
- Accuracy: Metrics should reflect reality
Considered Options
Section titled “Considered Options”Option 1: Keep ExpandableFeatureCard as-is (Current)
Section titled “Option 1: Keep ExpandableFeatureCard as-is (Current)”Pros:
- Already optimal: native
<details>, deferred module - ~1KB gzipped is negligible
- No hydration overhead
- Progressive enhancement pattern
Cons:
- Inline script duplicated per card (mitigated by module bundling)
Option 2: Extract script to external file
Section titled “Option 2: Extract script to external file”Pros:
- Single script reference
- Better caching
Cons:
- Adds HTTP request (minimal with HTTP/2)
- Over-engineering for 1KB
- Loses component encapsulation
Option 3: Remove JS entirely (CSS-only)
Section titled “Option 3: Remove JS entirely (CSS-only)”Pros:
- True zero-JS
- Simplest possible
Cons:
- Loses synchronized expand/collapse UX
- Degrades user experience
- Not worth the tradeoff for 1KB
Option 4: Dynamic metric generation from Lighthouse CI
Section titled “Option 4: Dynamic metric generation from Lighthouse CI”Pros:
- Always accurate scores
- Automated updates
- No manual maintenance
Cons:
- Adds build complexity
- Scores vary by environment
- Misleading if content changes
- Requires Lighthouse CI integration
Option 5: Move metrics to content collections
Section titled “Option 5: Move metrics to content collections”Pros:
- Centralized data management
- Type-safe schemas
- Easier to update
Cons:
- Over-engineering for static marketing data
- Adds query overhead
- No real benefit for homepage data
Decisions
Section titled “Decisions”1. Keep ExpandableFeatureCard Script (Option 1)
Section titled “1. Keep ExpandableFeatureCard Script (Option 1)”Amendment (2026-07-05): The sync script was later extracted to
src/scripts/featureCardSync.ts(Option 2’s file layout) but the import was lost in the move, leaving the behaviour silently dead. The module is now imported fromExpandableFeatureCard.astro’s<script>block — Astro dedupes module scripts, so the “loaded once per page” property this decision valued is preserved with either layout. The behaviour is guarded bysrc/scripts/__tests__/featureCardSync.test.tsand an e2e assertion ine2e/index.spec.ts.
Rationale:
- Current implementation is already optimal
- ~1KB gzipped is well within performance budget
- Uses native HTML
<details>with progressive enhancement - Deferred module loading is non-blocking
- No hydration overhead (pure DOM manipulation)
Implementation:
<!-- Native HTML with JS enhancement --><details class="feature-details"> <summary>Show details</summary> <div>Content</div></details>
<script type="module"> // Deferred, non-blocking enhancement const syncFeatureCards = () => { /* ... */ }; // ...</script>2. No Additional Lazy Loading Needed
Section titled “2. No Additional Lazy Loading Needed”Amendment (2026-08-02):
index.astrohas since gained three SVG lifecycle portraits (ADR-054), rendered deliberately via native<img>rather than<Image>— Astro’s Image component rasterizes SVG, which would kill their embedded CSS animations. The “no images” analysis in this record reflects the page at decision time.
Rationale:
- Index page has no images (only inline SVG icons)
- No iframes present
- Future images should use
<Image loading="lazy">by default - Astro’s Image component handles optimization automatically
Guideline for future additions:
<!-- Correct pattern for future images --><Image src={heroImage} alt="Description" loading="lazy" // Default for below-fold images format="avif" // Automatic via Astro config/>3. Keep Metrics Hardcoded (Reject Option 4)
Section titled “3. Keep Metrics Hardcoded (Reject Option 4)”Rationale:
- Lighthouse scores are marketing claims, not live data
- Scores reflect “ideal conditions (empty starter)” - documented
- Dynamic generation would be misleading as content grows
- Manual updates force conscious decisions about claims
- Simpler build process
Implementation:
/** * Lighthouse performance metrics for the starter template. * Reflects ideal conditions with empty starter content. */const metrics: LighthouseMetric[] = [ { label: "Performance", score: "95+", icon: "🚀" }, // Manually updated, consciously maintained];Disclaimer added:
“Scores reflect ideal conditions (empty starter). Real-world results may vary by deployment and content.”
4. Keep Data in Frontmatter (Reject Option 5)
Section titled “4. Keep Data in Frontmatter (Reject Option 5)”Rationale:
- Homepage data is static marketing content, not dynamic content
- Content collections are for blog posts, projects, etc.
- Frontmatter keeps data co-located with usage
- Type safety achieved via
src/types/content.ts - No query overhead
5. Implement Prefetch/Preconnect for External Links
Section titled “5. Implement Prefetch/Preconnect for External Links”(Amended 2026-08-13: the homepage no longer passes preconnectDomains — the GitHub preconnect was removed in a later pass. The Head.astro prop remains available; this section records the founding implementation.)
Rationale:
- External links to GitHub and documentation domains benefit from DNS prefetch
- Internal documentation links benefit from Astro’s prefetch integration
- Reduces latency by 100-300ms for external link clicks
- Enables instant navigation for internal links
Implementation:
<BaseLayout title="..." description="..." preconnectDomains={["https://github.com"]}> <!-- Internal links with prefetch --> <Button href="/docs/getting-started" data-astro-prefetch> View Documentation </Button>
<!-- External links with security attributes --> <Button href="https://github.com/..." rel="noopener noreferrer" target="_blank"> View on GitHub </Button></BaseLayout>Generated HTML:
<head> <!-- DNS prefetch for faster domain resolution --> <link rel="dns-prefetch" href="https://github.com" /> <link rel="preconnect" href="https://github.com" crossorigin /> <!-- Add preconnect for your docs domain if hosted externally --> <!-- <link rel="dns-prefetch" href="https://your-docs-site.example.com" /> --> <!-- <link rel="preconnect" href="https://your-docs-site.example.com" crossorigin /> --></head>Performance Budget Compliance
Section titled “Performance Budget Compliance”Current homepage metrics:
- Total JS: ~1KB gzipped (homepage-specific)
- View Transitions: ~5KB gzipped (framework feature)
- CSS: ~4KB gzipped
- Images: 0 (SVG icons only)
Budget status:
- ✅ JS Budget: 160KB total raw (using ~17KB raw = ~11%)
- ✅ CSS Budget: 50KB (using ~4KB = 8%)
- ✅ Image Budget: 200KB (using 0KB = 0%)
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- Optimal performance: Minimal JS, native HTML features
- Maintainable: Clear data structures with type safety
- Honest metrics: Hardcoded scores with clear disclaimers
- Progressive enhancement: Works without JS, better with JS
- Simple build: No complex metric generation
Neutral/To Address
Section titled “Neutral/To Address”- Manual updates: Metrics require conscious maintenance
- Mitigated by: Type safety and documentation
- Benefit: Forces honest evaluation of claims
- Script per card: Module bundling handles this efficiently
- Actual cost: ~1KB total, not per card
Negative
Section titled “Negative”- None identified - current implementation is optimal
Monitoring and Validation
Section titled “Monitoring and Validation”Performance checks:
# Build and check bundle sizespnpm run buildls -lh dist/_astro/*.js
# Run Lighthousepnpm run perf:lighthouse
# Check performance budgetspnpm run perf:budgetsValidation criteria:
- ✅ No
client:*directives on index.astro - ✅ No Preact/React islands
- ✅ Total JS < 10KB gzipped (homepage-specific)
- ✅ Lighthouse Performance > 95
- ✅ Native HTML features used where possible
Future Considerations
Section titled “Future Considerations”If adding images to index.astro:
<Image src={image} alt="Description" loading="lazy" // For below-fold images loading="eager" // For above-fold hero images format={["avif", "webp"]}/>If adding interactive components:
- Prefer CSS-only solutions
- Use
client:visiblefor below-fold interactivity - Use
client:idlefor non-critical enhancements - Never use
client:loadwithout ADR justification - Document decision in this ADR
If metrics become dynamic:
- Create ADR justifying the complexity
- Implement Lighthouse CI integration
- Add caching strategy
- Update disclaimer language
References
Section titled “References”- Astro Islands Architecture
- HTML Details Element (MDN)
- Progressive Enhancement Principles
- ADR-001: Preact Island Usage Policy
- Internal:
src/components/molecules/ExpandableFeatureCard.astro - Internal:
src/pages/index.astro
Enforcement
Section titled “Enforcement”- Not machine-checkable: per-component hydration judgment calls; the aggregate is protected by the JS bundle budget gate (pre-existing) and the Lighthouse workflow.
- Graduation log: (empty at creation; entries added when a check changes status)
Date: 2025-10-01 (footer backfilled 2026-07-05 from git history; this record predates the footer convention)
Participants: Template maintainers
Outcome: Accepted