Files
gh-dhofheinz-open-plugins-p…/commands/optimize/frontend.md
2025-11-29 18:20:21 +08:00

20 KiB

Frontend Optimization Operation

You are executing the frontend operation to optimize frontend bundle size, rendering performance, asset loading, and Web Vitals.

Parameters

Received: $ARGUMENTS (after removing 'frontend' operation name)

Expected format: target:"bundles|rendering|assets|images|fonts|all" [pages:"page-list"] [metrics_target:"lighthouse-score"] [framework:"react|vue|angular|svelte"]

Parameter definitions:

  • target (required): What to optimize - bundles, rendering, assets, images, fonts, or all
  • pages (optional): Specific pages to optimize (comma-separated, e.g., "dashboard,profile,checkout")
  • metrics_target (optional): Target Lighthouse score (e.g., "lighthouse>90", "lcp<2.5s")
  • framework (optional): Framework being used - react, vue, angular, svelte (auto-detected if not specified)

Workflow

1. Detect Frontend Framework and Build Tool

# Check framework
grep -E "\"react\"|\"vue\"|\"@angular\"|\"svelte\"" package.json | head -5

# Check build tool
grep -E "\"webpack\"|\"vite\"|\"parcel\"|\"rollup\"|\"esbuild\"" package.json | head -5

# Check for Next.js, Nuxt, etc.
ls next.config.js nuxt.config.js vite.config.js webpack.config.js 2>/dev/null

2. Run Performance Audit

Lighthouse Audit:

# Single page audit
npx lighthouse https://your-app.com --output=json --output-path=./audit-baseline.json --view

# Multiple pages
for page in dashboard profile checkout; do
  npx lighthouse "https://your-app.com/$page" \
    --output=json \
    --output-path="./audit-$page.json"
done

# Use Lighthouse CI for automated audits
npm install -g @lhci/cli
lhci autorun --config=lighthouserc.json

Bundle Analysis:

# Webpack Bundle Analyzer
npm run build -- --stats
npx webpack-bundle-analyzer dist/stats.json

# Vite bundle analysis
npx vite-bundle-visualizer

# Next.js bundle analysis
npm install @next/bundle-analyzer
# Then configure in next.config.js

3. Bundle Optimization

3.1. Code Splitting by Route

React (with React Router):

// BEFORE (everything in one bundle)
import Dashboard from './pages/Dashboard';
import Profile from './pages/Profile';
import Settings from './pages/Settings';

function App() {
  return (
    <Routes>
      <Route path="/dashboard" element={<Dashboard />} />
      <Route path="/profile" element={<Profile />} />
      <Route path="/settings" element={<Settings />} />
    </Routes>
  );
}
// Result: 2.5MB initial bundle

// AFTER (lazy loading by route)
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/profile" element={<Profile />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}
// Result: 450KB initial + 3 smaller chunks
// Improvement: 82% smaller initial bundle

Next.js (automatic code splitting):

// Next.js automatically splits by page, but you can add dynamic imports:
import dynamic from 'next/dynamic';

const HeavyComponent = dynamic(() => import('../components/HeavyChart'), {
  loading: () => <p>Loading chart...</p>,
  ssr: false // Don't render on server if not needed
});

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <HeavyComponent data={data} />
    </div>
  );
}

Vue (with Vue Router):

// BEFORE
import Dashboard from './views/Dashboard.vue';
import Profile from './views/Profile.vue';

const routes = [
  { path: '/dashboard', component: Dashboard },
  { path: '/profile', component: Profile }
];

// AFTER (lazy loading)
const routes = [
  { path: '/dashboard', component: () => import('./views/Dashboard.vue') },
  { path: '/profile', component: () => import('./views/Profile.vue') }
];

3.2. Tree Shaking and Dead Code Elimination

Proper Import Strategy:

// BEFORE (imports entire library)
import _ from 'lodash'; // 70KB
import moment from 'moment'; // 232KB
import { Button, Modal, Table, Form, Input } from 'antd'; // Imports all

const formatted = moment().format('YYYY-MM-DD');
const debounced = _.debounce(fn, 300);

// AFTER (tree-shakeable imports)
import { debounce } from 'lodash-es'; // 2KB (tree-shakeable)
import { format } from 'date-fns'; // 12KB (tree-shakeable)
import Button from 'antd/es/button'; // Import only what's needed
import Modal from 'antd/es/modal';

const formatted = format(new Date(), 'yyyy-MM-dd');
const debounced = debounce(fn, 300);

// Bundle size reduction: ~290KB → ~20KB (93% smaller)

Webpack Configuration:

// webpack.config.js
module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true, // Tree shaking
    sideEffects: false, // Assume no side effects (check package.json)
    minimize: true,
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10
        },
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        }
      }
    }
  }
};

3.3. Remove Unused Dependencies

# Analyze unused dependencies
npx depcheck

# Example output:
# Unused dependencies:
#   * moment (use date-fns instead)
#   * jquery (not used in React app)
#   * bootstrap (using Tailwind instead)

# Remove them
npm uninstall moment jquery bootstrap

# Check bundle impact
npm run build

3.4. Optimize Bundle Chunks

// Vite config for optimal chunking
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vendor-react': ['react', 'react-dom', 'react-router-dom'],
          'vendor-ui': ['antd', '@ant-design/icons'],
          'vendor-utils': ['axios', 'lodash-es', 'date-fns']
        }
      }
    },
    chunkSizeWarningLimit: 500 // Warn if chunk > 500KB
  }
});

// Next.js config for optimal chunking
module.exports = {
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.optimization.splitChunks = {
        chunks: 'all',
        cacheGroups: {
          default: false,
          vendors: false,
          framework: {
            name: 'framework',
            chunks: 'all',
            test: /(?<!node_modules.*)[\\/]node_modules[\\/](react|react-dom|scheduler|prop-types)[\\/]/,
            priority: 40,
            enforce: true
          },
          lib: {
            test: /[\\/]node_modules[\\/]/,
            name: 'lib',
            priority: 30,
            minChunks: 1,
            reuseExistingChunk: true
          }
        }
      };
    }
    return config;
  }
};

4. Rendering Optimization

4.1. React - Prevent Unnecessary Re-renders

Memoization:

// BEFORE (re-renders on every parent update)
function UserList({ users, onSelect }) {
  return users.map(user => (
    <UserCard key={user.id} user={user} onSelect={onSelect} />
  ));
}

function UserCard({ user, onSelect }) {
  console.log('Rendering UserCard:', user.id);
  return (
    <div onClick={() => onSelect(user)}>
      {user.name} - {user.email}
    </div>
  );
}
// Result: All cards re-render even if only one user changes

// AFTER (memoized components)
import { memo, useCallback, useMemo } from 'react';

const UserCard = memo(({ user, onSelect }) => {
  console.log('Rendering UserCard:', user.id);
  return (
    <div onClick={() => onSelect(user)}>
      {user.name} - {user.email}
    </div>
  );
});

function UserList({ users, onSelect }) {
  const memoizedOnSelect = useCallback(onSelect, []); // Stable reference

  return users.map(user => (
    <UserCard key={user.id} user={user} onSelect={memoizedOnSelect} />
  ));
}
// Result: Only changed cards re-render
// Performance: 90% fewer renders for 100 cards

useMemo for Expensive Computations:

// BEFORE (recalculates on every render)
function Dashboard({ data }) {
  const stats = calculateComplexStats(data); // Expensive: 50ms

  return <StatsDisplay stats={stats} />;
}
// Result: 50ms wasted on every render, even if data unchanged

// AFTER (memoized calculation)
function Dashboard({ data }) {
  const stats = useMemo(
    () => calculateComplexStats(data),
    [data] // Only recalculate when data changes
  );

  return <StatsDisplay stats={stats} />;
}
// Result: 0ms for unchanged data, 50ms only when data changes

4.2. Virtual Scrolling for Long Lists

// BEFORE (renders all 10,000 items)
function LargeList({ items }) {
  return (
    <div className="list">
      {items.map(item => (
        <ListItem key={item.id} data={item} />
      ))}
    </div>
  );
}
// Result: Initial render: 2,500ms, 10,000 DOM nodes

// AFTER (virtual scrolling with react-window)
import { FixedSizeList } from 'react-window';

function LargeList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      <ListItem data={items[index]} />
    </div>
  );

  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}
// Result: Initial render: 45ms, only ~20 visible DOM nodes
// Performance: 98% faster, 99.8% fewer DOM nodes

4.3. Debounce Expensive Operations

// BEFORE (triggers on every keystroke)
function SearchBox() {
  const [query, setQuery] = useState('');

  const handleSearch = (value) => {
    setQuery(value);
    fetchResults(value); // API call on every keystroke
  };

  return <input onChange={(e) => handleSearch(e.target.value)} />;
}
// Result: 50 API calls for typing "performance optimization"

// AFTER (debounced search)
import { useMemo } from 'react';
import { debounce } from 'lodash-es';

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

  const debouncedSearch = useMemo(
    () => debounce((value) => fetchResults(value), 300),
    []
  );

  const handleSearch = (value) => {
    setQuery(value);
    debouncedSearch(value);
  };

  return <input onChange={(e) => handleSearch(e.target.value)} />;
}
// Result: 1-2 API calls for typing "performance optimization"
// Performance: 96% fewer API calls

5. Image Optimization

5.1. Modern Image Formats

// BEFORE (traditional formats)
<img src="/images/hero.jpg" alt="Hero" />
// hero.jpg: 1.2MB

// AFTER (modern formats with fallback)
<picture>
  <source srcset="/images/hero.avif" type="image/avif" />
  <source srcset="/images/hero.webp" type="image/webp" />
  <img src="/images/hero.jpg" alt="Hero" loading="lazy" />
</picture>
// hero.avif: 180KB (85% smaller)
// hero.webp: 240KB (80% smaller)

Next.js Image Optimization:

// BEFORE
<img src="/hero.jpg" alt="Hero" />

// AFTER (automatic optimization)
import Image from 'next/image';

<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority // Load immediately for above-fold images
  placeholder="blur" // Show blur while loading
  blurDataURL="data:image/..." // Inline blur placeholder
/>
// Automatically serves WebP/AVIF based on browser support

5.2. Lazy Loading

// BEFORE (all images load immediately)
<div className="gallery">
  {images.map(img => (
    <img key={img.id} src={img.url} alt={img.title} />
  ))}
</div>
// Result: 50 images load on page load (slow)

// AFTER (native lazy loading)
<div className="gallery">
  {images.map(img => (
    <img
      key={img.id}
      src={img.url}
      alt={img.title}
      loading="lazy" // Native browser lazy loading
    />
  ))}
</div>
// Result: Only visible images load initially
// Performance: 85% fewer initial network requests

5.3. Responsive Images

// BEFORE (serves same large image to all devices)
<img src="/hero-2400w.jpg" alt="Hero" />
// Mobile: Downloads 2.4MB image for 375px screen

// AFTER (responsive srcset)
<img
  src="/hero-800w.jpg"
  srcset="
    /hero-400w.jpg 400w,
    /hero-800w.jpg 800w,
    /hero-1200w.jpg 1200w,
    /hero-2400w.jpg 2400w
  "
  sizes="
    (max-width: 600px) 400px,
    (max-width: 900px) 800px,
    (max-width: 1200px) 1200px,
    2400px
  "
  alt="Hero"
/>
// Mobile: Downloads 120KB image for 375px screen
// Performance: 95% smaller download on mobile

6. Asset Optimization

6.1. Font Loading Strategy

/* BEFORE (blocks rendering) */
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');

/* AFTER (optimized loading) */
/* Use font-display: swap to show fallback text immediately */
@font-face {
  font-family: 'Roboto';
  src: url('/fonts/roboto.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap; /* Show text immediately with fallback font */
}

/* Preload critical fonts in HTML */
<link rel="preload" href="/fonts/roboto.woff2" as="font" type="font/woff2" crossorigin>

Variable Fonts (single file for multiple weights):

/* BEFORE (multiple files) */
/* roboto-regular.woff2: 50KB */
/* roboto-bold.woff2: 52KB */
/* roboto-light.woff2: 48KB */
/* Total: 150KB */

/* AFTER (variable font) */
@font-face {
  font-family: 'Roboto';
  src: url('/fonts/roboto-variable.woff2') format('woff2-variations');
  font-weight: 300 700; /* Supports all weights from 300-700 */
}
/* roboto-variable.woff2: 75KB */
/* Savings: 50% smaller */

6.2. Critical CSS

<!-- BEFORE (blocks rendering until full CSS loads) -->
<link rel="stylesheet" href="/styles/main.css"> <!-- 250KB -->

<!-- AFTER (inline critical CSS, defer non-critical) -->
<style>
  /* Inline critical above-the-fold CSS (< 14KB) */
  .header { ... }
  .hero { ... }
  .nav { ... }
</style>

<!-- Defer non-critical CSS -->
<link rel="preload" href="/styles/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles/main.css"></noscript>

<!-- Or use media query trick -->
<link rel="stylesheet" href="/styles/main.css" media="print" onload="this.media='all'">

6.3. JavaScript Defer/Async

<!-- BEFORE (blocks HTML parsing) -->
<script src="/js/analytics.js"></script>
<script src="/js/chat-widget.js"></script>
<script src="/js/app.js"></script>

<!-- AFTER (non-blocking) -->
<!-- async: Download in parallel, execute as soon as ready (order not guaranteed) -->
<script src="/js/analytics.js" async></script>
<script src="/js/chat-widget.js" async></script>

<!-- defer: Download in parallel, execute after HTML parsed (order guaranteed) -->
<script src="/js/app.js" defer></script>

<!-- Performance: Eliminates script blocking time -->

7. Caching and Service Workers

Service Worker for Offline Support:

// sw.js
const CACHE_NAME = 'app-v1';
const urlsToCache = [
  '/',
  '/styles/main.css',
  '/js/app.js',
  '/images/logo.png'
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(urlsToCache))
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // Return cached version or fetch from network
      return response || fetch(event.request);
    })
  );
});

// Register in app
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js');
}

8. Web Vitals Optimization

Optimize LCP (Largest Contentful Paint < 2.5s):

  • Preload critical resources: <link rel="preload" href="hero.jpg" as="image">
  • Use CDN for static assets
  • Optimize server response time (TTFB < 600ms)
  • Optimize images (modern formats, compression)

Optimize FID/INP (First Input Delay / Interaction to Next Paint < 200ms):

  • Reduce JavaScript execution time
  • Break up long tasks (yield to main thread)
  • Use web workers for heavy computation
  • Debounce/throttle event handlers

Optimize CLS (Cumulative Layout Shift < 0.1):

  • Set explicit width/height for images and videos
  • Reserve space for dynamic content
  • Avoid inserting content above existing content
  • Use CSS aspect-ratio for responsive media
/* Prevent CLS for images */
img {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9; /* Reserve space before image loads */
}

Output Format

# Frontend Optimization Report: [Context]

**Optimization Date**: [Date]
**Framework**: [React/Vue/Angular version]
**Build Tool**: [Webpack/Vite/Next.js version]
**Target Pages**: [List of pages]

## Executive Summary

[Summary of findings and optimizations]

## Baseline Metrics

### Lighthouse Scores (Before)

| Page | Performance | Accessibility | Best Practices | SEO |
|------|-------------|---------------|----------------|-----|
| Home | 62 | 88 | 79 | 92 |
| Dashboard | 48 | 91 | 75 | 89 |
| Profile | 55 | 90 | 82 | 91 |

### Web Vitals (Before)

| Page | LCP | FID | CLS | TTFB |
|------|-----|-----|-----|------|
| Home | 4.2s | 180ms | 0.18 | 950ms |
| Dashboard | 5.8s | 320ms | 0.25 | 1200ms |

### Bundle Sizes (Before)

| Bundle | Size (gzipped) | Percentage |
|--------|----------------|------------|
| main.js | 850KB | 68% |
| vendor.js | 320KB | 25% |
| styles.css | 85KB | 7% |
| **Total** | **1.25MB** | **100%** |

## Optimizations Implemented

### 1. Implemented Code Splitting

**Before**: Single 850KB main bundle
**After**: Initial 180KB + route chunks (120KB, 95KB, 85KB)

**Impact**: 79% smaller initial bundle

### 2. Replaced Heavy Dependencies

- Moment.js (232KB) → date-fns (12KB) = 94.8% smaller
- Lodash (70KB) → lodash-es tree-shakeable (2KB used) = 97.1% smaller
- Total savings: 288KB

### 3. Implemented Virtual Scrolling

**User List (10,000 items)**:
- Before: 2,500ms initial render, 10,000 DOM nodes
- After: 45ms initial render, ~20 visible DOM nodes
- **Improvement**: 98% faster

### 4. Optimized Images

**Hero Image**:
- Before: hero.jpg (1.2MB)
- After: hero.avif (180KB)
- **Savings**: 85%

**Implemented**:
- Modern formats (WebP, AVIF)
- Lazy loading for below-fold images
- Responsive srcset for different screen sizes

### 5. Optimized Rendering with React.memo

**Product Grid (500 items)**:
- Before: All 500 components re-render on filter change
- After: Only filtered subset re-renders (~50 items)
- **Improvement**: 90% fewer re-renders

## Results Summary

### Lighthouse Scores (After)

| Page | Performance | Accessibility | Best Practices | SEO | Improvement |
|------|-------------|---------------|----------------|-----|-------------|
| Home | 94 (+32) | 95 (+7) | 92 (+13) | 100 (+8) | +32 points |
| Dashboard | 89 (+41) | 95 (+4) | 92 (+17) | 96 (+7) | +41 points |
| Profile | 91 (+36) | 95 (+5) | 92 (+10) | 100 (+9) | +36 points |

### Web Vitals (After)

| Page | LCP | FID | CLS | TTFB | Improvement |
|------|-----|-----|-----|------|-------------|
| Home | 1.8s | 45ms | 0.02 | 320ms | 57% faster LCP |
| Dashboard | 2.1s | 65ms | 0.04 | 450ms | 64% faster LCP |

### Bundle Sizes (After)

| Bundle | Size (gzipped) | Change |
|--------|----------------|--------|
| main.js | 180KB | -79% |
| vendor-react.js | 95KB | New |
| vendor-ui.js | 85KB | New |
| styles.css | 45KB | -47% |
| **Total Initial** | **405KB** | **-68%** |

### Load Time Improvements

| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| Initial Bundle Load | 3.8s | 1.2s | 68% faster |
| Time to Interactive | 6.5s | 2.3s | 65% faster |
| First Contentful Paint | 2.1s | 0.8s | 62% faster |
| Largest Contentful Paint | 4.2s | 1.8s | 57% faster |

## Trade-offs and Considerations

**Code Splitting**:
- **Benefit**: 68% smaller initial bundle
- **Trade-off**: Additional network requests for route chunks
- **Mitigation**: Chunks are cached, prefetch likely routes

**Image Format Optimization**:
- **Benefit**: 85% smaller images
- **Trade-off**: Build step complexity (convert to AVIF/WebP)
- **Fallback**: JPEG fallback for older browsers

## Monitoring Recommendations

1. **Real User Monitoring** for Web Vitals
2. **Lighthouse CI** in pull request checks
3. **Bundle size tracking** in CI/CD
4. **Performance budgets** (e.g., initial bundle < 500KB)

## Next Steps

1. Implement service worker for offline support
2. Add resource hints (prefetch, preconnect)
3. Consider migrating to Next.js for automatic optimizations
4. Implement CDN for static assets