frontend-landing/bundling-frontend/SKILL.md
# Bundling Frontend — Optimisation & Deployment > Ship a 60 fps storybook experience that loads fast on 3G, scores > 90+ on Lighthouse, and deploys in one command. --- ## Overview An animation-heavy, full-screen landing page has specific bundling concerns that differ from a typical content site: | Concern | Why It Matters | | ------------------------ | -------------------------------------------------- | | **JS bundle size** | Frame
npx skillsauth add 7a336e6e/skills frontend-landing/bundling-frontendInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Ship a 60 fps storybook experience that loads fast on 3G, scores 90+ on Lighthouse, and deploys in one command.
An animation-heavy, full-screen landing page has specific bundling concerns that differ from a typical content site:
| Concern | Why It Matters |
| ------------------------ | -------------------------------------------------- |
| JS bundle size | Framer Motion + React can push past 150 kB gzipped |
| CSS payload | Hundreds of keyframes and utility classes |
| First paint speed | Intro animation must start within ~1.5 s |
| Hydration cost | Full "use client" tree hydrates on load |
| Asset loading | Images, fonts, icons must not block the intro |
"use client" Boundary StrategyPush the
"use client"boundary as low as possible.
app/
layout.tsx ← Server Component (metadata, fonts, analytics scripts)
page.tsx ← Server Component (renders the client journey)
components/
seer/
seer-journey.tsx ← "use client" — the single interactive root
Only the storybook orchestrator and its children need to be client components. Everything above it (layout, metadata, font loading) stays on the server.
// ❌ Don't mark layout.tsx as "use client" just because a child needs it
"use client"; // app/layout.tsx — wastes server rendering
If certain scenes use large libraries (e.g., a chart library, 3D renderer), lazy-load them:
import dynamic from "next/dynamic";
const StatsScene = dynamic(() => import("./scenes/stats-scene"), {
loading: () => <SceneSkeleton />,
ssr: false, // No SSR for animation-heavy components
});
const DemoScene = dynamic(() => import("./scenes/demo-scene"), {
loading: () => <SceneSkeleton />,
ssr: false,
});
| Scenario | Recommendation |
| --------------------------------- | ----------------------- |
| Scene uses only Tailwind + motion | Keep inline (no split) |
| Scene imports a chart / 3D lib | dynamic() import |
| Scene has a heavy embed (iframe) | dynamic() + ssr: false |
| Mascot with pupil tracking only | Keep inline |
Import only what you use — framer-motion supports deep imports:
// ✅ Good — tree-shakeable
import { motion, AnimatePresence } from "framer-motion";
// ❌ Bad — pulls entire library (older pattern)
import * as Motion from "framer-motion";
As of framer-motion v11+, the main entry point is already
tree-shakeable, but avoid importing *.
Tailwind's JIT compiler only includes classes you actually use.
Ensure content paths cover all component files:
// tailwind.config.ts
const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
// …
};
Prefer CSS @keyframes for anything that runs continuously or on
non-interactive elements (backgrounds, ambient effects):
| Animation Type | CSS or JS? | Reason |
| --------------------- | ---------------- | ------------------------------- |
| Typewriter text | CSS clip-path | Zero re-renders |
| Floating particles | CSS @keyframes | Offloaded to compositor |
| Pulse / glow loops | CSS @keyframes | No JS overhead |
| Scene transitions | Framer Motion | Needs AnimatePresence logic |
| Scroll-linked effects | Framer Motion | Needs useScroll / useMotionValue |
| Mascot eye tracking | JS (ref-based) | Needs pointer position |
will-change for Composited LayersApply will-change to elements that animate transform or opacity:
.scene-container {
will-change: transform, opacity;
}
.mascot-eye {
will-change: transform;
}
⚠️ Don't apply
will-changeto everything — it consumes GPU memory. Only use it on elements that actually animate.
contain.scene-page {
contain: layout style paint;
}
This tells the browser that the scene is self-contained, reducing paint calculations when other parts of the DOM change.
next/font for Zero-FOUT Loading// app/layout.tsx
import { Inter, JetBrains_Mono } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-sans",
});
const mono = JetBrains_Mono({
subsets: ["latin"],
display: "swap",
variable: "--font-mono",
});
export default function RootLayout({ children }) {
return (
<html className={`${inter.variable} ${mono.variable}`}>
<body>{children}</body>
</html>
);
}
next/font self-hosts fonts (no external requests)display: "swap" shows fallback text immediately// tailwind.config.ts
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],
mono: ["var(--font-mono)", ...fontFamily.mono],
},
// ✅ Import individual icons
import { Shield, Brain, Eye } from "lucide-react";
// ❌ Don't import the whole set
import * as Icons from "lucide-react";
Each icon is ~200 bytes gzipped. Importing only what you need keeps the bundle small.
Prefer CSS gradients over raster images for backgrounds:
/* ✅ Zero network requests, infinitely scalable */
.hero-bg {
background: radial-gradient(
ellipse at 30% 50%,
rgba(0, 212, 170, 0.08) 0%,
transparent 60%
);
}
/* ❌ Adds a network request and is fixed resolution */
.hero-bg {
background-image: url("/hero-bg.png");
}
import Image from "next/image";
<Image
src="/product-preview.png"
alt="Product dashboard"
width={800}
height={500}
priority={false} // Only priority for above-the-fold
placeholder="blur"
blurDataURL="data:image/png;base64,…" // Tiny placeholder
/>
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
// Enable gzip + brotli compression
compress: true,
// Optimise package imports
experimental: {
optimizePackageImports: [
"lucide-react",
"framer-motion",
],
},
// Security headers
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
],
},
];
},
};
export default nextConfig;
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
This catches unused imports and dead code at compile time, preventing bloat from accumulating.
Set targets and measure against them:
| Metric | Target | Tool |
| --------------------------- | -------------- | ----------------------- |
| First Contentful Paint | < 1.5 s | Lighthouse |
| Largest Contentful Paint| < 2.5 s | Lighthouse |
| Total Blocking Time | < 200 ms | Lighthouse |
| Cumulative Layout Shift | < 0.1 | Lighthouse |
| JS Bundle (gzipped) | < 180 kB | next build output |
| CSS (gzipped) | < 30 kB | next build output |
| Time to Interactive | < 3.0 s (3G) | WebPageTest |
# Build and see chunk breakdown
npx next build
# Detailed bundle analysis
ANALYZE=true npx next build
# Requires: npm install @next/bundle-analyzer
// next.config.mjs
import withBundleAnalyzer from "@next/bundle-analyzer";
const config = withBundleAnalyzer({
enabled: process.env.ANALYZE === "true",
})({
// … your config
});
export default config;
# Install Vercel CLI
npm i -g vercel
# Deploy (auto-detects Next.js)
vercel
# Production deploy
vercel --prod
Vercel provides:
If you don't need server-side features:
// next.config.mjs
const nextConfig = {
output: "export",
// Images need unoptimized for static export
images: { unoptimized: true },
};
npx next build
# Output in `out/` — deploy to any static host
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
Requires output: "standalone" in next.config.mjs.
npm run build completes without errorsconsole.log debug statements removedafterInteractive strategyrobots.txt and sitemap.xml configured# Development
npm run dev # Start dev server (Turbopack)
# Production build
npm run build # Build for production
npm run start # Start production server locally
# Analysis
ANALYZE=true npm run build # Bundle size analysis
# Linting
npm run lint # ESLint check
npx tsc --noEmit # Type check without build
Next: Testing Frontend →
development
Implement features using the Red-Green-Refactor cycle to ensure testability and correctness from the start.
data-ai
Manage the `tasks.md` ledger with strict locking and collision avoidance protocols to allow multiple agents to work in parallel safely.
development
The git-workflow skill defines branching conventions, commit message formats, and pull request standards that all agents must follow for consistent version control.
development
The environment-config skill standardizes how agents manage environment variables, secrets, and application configuration across local development and deployed environments.