All skills
github avatar

/spark-app-template

@53bfe2d
by githubgithub/copilot-plugins367 stars
136

Comprehensive guidance for building web apps with opinionated defaults for tech stack, design system, and code standards. Use when user wants to create a new web application, dashboard, or interactive interface. Provides tech choices, styling guidance, project structure, and design philosophy to get users up and running quickly with a fully functional, beautiful web app.

Use this Skill: https://skilld.dev/gh/github/copilot-plugins/spark-app-template

This session only. Nothing lands on disk.

referencesperformance-checklist.md

≈3.3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Performance Checklist - Spark App Template

Core Web Vitals 2026

Google's Core Web Vitals are essential metrics for user experience and SEO ranking.

Current Metrics

INP (Interaction to Next Paint) - Replaced FID in March 2024

  • Target: < 200ms (Good)
  • What it measures: Full interaction responsiveness (input delay + processing + paint)
  • Why it matters: Captures complete user interaction experience, not just first click

LCP (Largest Contentful Paint)

  • Target: < 2.5s (Good)
  • What it measures: Time for largest content element to render
  • Why it matters: Perceived loading performance

CLS (Cumulative Layout Shift)

  • Target: < 0.1 (Good)
  • What it measures: Visual stability (unexpected layout shifts)
  • Why it matters: User frustration from elements moving during interaction

Measuring Core Web Vitals

In Development:

# Chrome DevTools
1. Open DevTools (F12)
2. Lighthouse tab
3. Generate report
4. Check Performance score + Web Vitals

In Production:

// Install web-vitals
pnpm add web-vitals

// src/main.tsx
import { onCLS, onINP, onLCP } from 'web-vitals';

onCLS(console.log);
onINP(console.log);
onLCP(console.log);

// Or send to analytics
function sendToAnalytics(metric) {
  fetch('/analytics', {
    method: 'POST',
    body: JSON.stringify(metric),
  });
}

onINP(sendToAnalytics);

React 19 Compiler

What It Does

The React Compiler automatically memoizes components and values, eliminating manual useMemo, useCallback, and React.memo in most cases.

Performance Gains:

  • 12% faster initial loads
  • 2.5x faster interactions
  • Reduces unnecessary re-renders automatically

Enabling React Compiler

pnpm add -D babel-plugin-react-compiler
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [
          ['babel-plugin-react-compiler', {}],
        ],
      },
    }),
  ],
});

Verification

Check that components are compiled:

// You'll still write normal React
function MyComponent() {
  const [count, setCount] = useState(0);

  // Compiler automatically memoizes this
  const expensiveValue = count * 2;

  return <div>{expensiveValue}</div>;
}

// Compiler output (internal):
// - Tracks dependencies automatically
// - Memoizes render when dependencies don't change
// - No manual optimization needed

Image Optimization

Use Modern Formats

// WebP with fallback
<picture>
  <source srcSet="/image.webp" type="image/webp" />
  <source srcSet="/image.avif" type="image/avif" />
  <img src="/image.jpg" alt="Description" loading="lazy" />
</picture>

Responsive Images

<img
  srcSet="
    /image-400.jpg 400w,
    /image-800.jpg 800w,
    /image-1200.jpg 1200w
  "
  sizes="(max-width: 640px) 400px, (max-width: 1024px) 800px, 1200px"
  src="/image-800.jpg"
  alt="Description"
  loading="lazy"
/>

Lazy Loading

// Native lazy loading (built-in)
<img src="/image.jpg" loading="lazy" alt="Description" />

// Eager load above-the-fold images
<img src="/hero.jpg" loading="eager" alt="Hero" />

// Or priority loading
<img src="/hero.jpg" fetchpriority="high" alt="Hero" />

Image CDN

Use image CDNs for automatic optimization:

// Example: Cloudinary
<img
  src="https://res.cloudinary.com/demo/image/upload/w_400,f_auto,q_auto/sample.jpg"
  alt="Optimized"
/>

// w_400 = width 400px
// f_auto = automatic format (WebP/AVIF)
// q_auto = automatic quality

Font Optimization

Variable Fonts

<!-- One file, all weights -->
<link
  href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400..700&display=swap"
  rel="stylesheet"
/>

Benefits:

  • Single HTTP request
  • Smaller total file size
  • Smooth weight interpolation

Font Display Strategy

@font-face {
  font-family: 'Custom Font';
  src: url('/fonts/font.woff2') format('woff2');
  font-display: swap; /* Show fallback immediately */
}

Options:

  • swap: Best for most cases (show fallback, swap when loaded)
  • optional: Only use if cached (best for performance)
  • fallback: Brief invisible period, then fallback

Preload Critical Fonts

<head>
  <link
    rel="preload"
    href="/fonts/heading-font.woff2"
    as="font"
    type="font/woff2"
    crossorigin
  />
</head>

Only preload:

  • Hero/heading font
  • Above-the-fold text
  • Max 1-2 fonts

Font Subsetting

If self-hosting, subset fonts to include only needed characters:

# Install pyftsubset
pip install fonttools

# Subset to Latin characters only
pyftsubset font.ttf \
  --output-file=font-subset.woff2 \
  --flavor=woff2 \
  --layout-features=* \
  --unicodes=U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD

Code Splitting

Route-Based Splitting

TanStack Router handles this automatically:

// Each route is automatically code-split
export const Route = createFileRoute('/dashboard')({
  component: Dashboard, // Only loaded when route is accessed
});

Component Lazy Loading

import { lazy, Suspense } from 'react';

const HeavyChart = lazy(() => import('./HeavyChart'));

export function Dashboard() {
  return (
    <Suspense fallback={<ChartSkeleton />}>
      <HeavyChart data={data} />
    </Suspense>
  );
}

Dynamic Imports

// Load on demand
async function handleExport() {
  const { exportToPDF } = await import('./export-utils');
  exportToPDF(data);
}

Bundle Size Optimization

Analyze Bundle

pnpm add -D rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
    }),
  ],
});

Tree-Shaking Tips

// ❌ Bad: Imports entire library
import _ from 'lodash';

// ✅ Good: Import only what you need
import debounce from 'lodash/debounce';

// ❌ Bad: Imports all icons
import * as Icons from 'lucide-react';

// ✅ Good: Import specific icons
import { Home, Settings } from 'lucide-react';

Remove Unused Code

# Check unused dependencies
pnpm exec depcheck

# Remove unused exports
pnpm add -D ts-prune
pnpm exec ts-prune

TanStack Query Optimization

Caching Strategy

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000, // 1 minute
      gcTime: 5 * 60 * 1000, // 5 minutes (was cacheTime)
      refetchOnWindowFocus: false, // Disable aggressive refetching
    },
  },
});

Prefetching

import { queryOptions, useSuspenseQuery } from '@tanstack/react-query';

const userQueryOptions = queryOptions({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
});

// Prefetch on hover
<Link
  to="/user/$userId"
  params={{ userId }}
  onMouseEnter={() => {
    queryClient.prefetchQuery(userQueryOptions);
  }}
>
  View Profile
</Link>

Suspense for Data Loading

export function UserProfile({ userId }: { userId: string }) {
  // useSuspenseQuery throws promise, letting Suspense handle loading
  const { data } = useSuspenseQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

  return <div>{data.name}</div>;
}

// Parent component
<Suspense fallback={<ProfileSkeleton />}>
  <UserProfile userId={userId} />
</Suspense>

Animation Performance

Use GPU-Accelerated Properties

/* ✅ GPU-accelerated (fast) */
.animated {
  transform: translateX(100px);
  opacity: 0.5;
}

/* ❌ CPU-bound (slow) */
.animated {
  margin-left: 100px;
  width: 200px;
}

Debounce Expensive Operations

import { debounce } from 'lodash';
import { useEffect, useMemo } from 'react';

export function SearchInput() {
  const [query, setQuery] = useState('');

  const debouncedSearch = useMemo(
    () => debounce((value: string) => {
      // Expensive search operation
      performSearch(value);
    }, 300),
    []
  );

  useEffect(() => {
    return () => debouncedSearch.cancel();
  }, [debouncedSearch]);

  return (
    <input
      value={query}
      onChange={(e) => {
        setQuery(e.target.value);
        debouncedSearch(e.target.value);
      }}
    />
  );
}

Use will-change Sparingly

/* Only during animation */
.animating {
  will-change: transform;
}

/* Remove after animation */
.animated {
  will-change: auto;
}

Rendering Optimization

Virtual Scrolling (Large Lists)

pnpm add @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';

export function VirtualList({ items }: { items: any[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
  });

  return (
    <div ref={parentRef} className="h-[600px] overflow-auto">
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualItem.size}px`,
              transform: `translateY(${virtualItem.start}px)`,
            }}
          >
            {items[virtualItem.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}

Avoid Inline Functions in Render

// ❌ Bad: Creates new function every render
<Button onClick={() => handleClick(item.id)}>Click</Button>

// ✅ Good: Memoized function (if React Compiler not enabled)
const handleClickItem = useCallback(() => {
  handleClick(item.id);
}, [item.id]);

<Button onClick={handleClickItem}>Click</Button>

// ✅ Best: With React Compiler, write naturally
<Button onClick={() => handleClick(item.id)}>Click</Button>
// Compiler auto-memoizes this

Network Optimization

API Request Batching

// Batch multiple queries
const results = useQueries({
  queries: [
    { queryKey: ['user', userId], queryFn: fetchUser },
    { queryKey: ['posts', userId], queryFn: fetchPosts },
    { queryKey: ['comments', userId], queryFn: fetchComments },
  ],
});

Request Deduplication

TanStack Query automatically deduplicates identical concurrent requests.

Compression

Ensure server sends compressed responses:

Content-Encoding: gzip
Content-Encoding: br (Brotli, even better)

Production Build Optimization

Vite Build Settings

// vite.config.ts
export default defineConfig({
  build: {
    target: 'es2020',
    minify: 'terser',
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          router: ['@tanstack/react-router'],
          ui: ['lucide-react', 'motion'],
        },
      },
    },
  },
});

Environment-Specific Code

if (import.meta.env.DEV) {
  // Development-only code (stripped in production)
  console.log('Debug info');
}

if (import.meta.env.PROD) {
  // Production-only code
  initAnalytics();
}

Performance Checklist

Use this checklist before deploying:

  • Core Web Vitals: INP < 200ms, LCP < 2.5s, CLS < 0.1
  • React Compiler: Enabled and verified working
  • Images: Using WebP/AVIF, lazy loaded, responsive
  • Fonts: Variable fonts, font-display: swap, preload critical
  • Bundle Size: Analyzed, < 200KB initial bundle gzipped
  • Code Splitting: Routes split, heavy components lazy loaded
  • Caching: TanStack Query configured, stale times set
  • Animations: GPU-accelerated properties only
  • Virtual Scrolling: Implemented for lists > 100 items
  • Network: Requests batched, compression enabled
  • Production Build: Minified, tree-shaken, optimized chunks

Monitoring

Set up continuous monitoring:

// Track Core Web Vitals
import { onCLS, onINP, onLCP } from 'web-vitals';

const sendToAnalytics = (metric) => {
  // Send to your analytics service
  fetch('/api/analytics', {
    method: 'POST',
    body: JSON.stringify(metric),
  });
};

onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);

Tools:

  • Google Search Console (real user metrics)
  • Chrome DevTools (Lighthouse)
  • WebPageTest (comprehensive testing)
  • Vercel Analytics (if deploying on Vercel)

Remember: Performance is a feature, not an afterthought. Budget your performance from the start, measure continuously, and optimize based on real-world data, not assumptions.

Source: SKILL.md on GitHub

1 warning5d5 checks · Risk SAFE
  • Gen Agent Trust Hub5d

    This skill provides a comprehensive and secure boilerplate for developing modern React web applications. It utilizes industry-standard tools like Vite, Tailwind CSS, and shadcn/ui, while explicitly incorporating security best practices such as HTML sanitization for content rendering.

  • Socket5d

    No alerts

  • Snyk5d

    Risk: LOW · No issues

  • Runlayer6mo

    3/12 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 53bfe2d. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub last month.

Steadyupdated 8 months ago

README badge

README badge for github/copilot-plugins/spark-app-template