Skip to content

Vibe coding: shipping faster without shipping garbage

August 5, 2025 (1y ago)

486 views

This wasn't a sprint or a grand plan. I browsed my site like a visitor, found the rough spots (CV out of date, blog light on content, slow pages, Lighthouse a11y warnings, weak SEO), and polished them one at a time. It's closer to detailing a car than building one: fix a scuff, step back, spot the next one.

The twist is that I gave an AI assistant the gruntwork and kept the intent and the review for myself.

What actually moved the needle

1) Images, the highest return by far

  • Converted PNG and JPEG assets to WebP, with <picture> fallbacks.
  • Set explicit width and height to kill layout shift, plus loading="lazy" and decoding="async" where it was safe.
  • Right-sized the oversized art and logos, and kept photos less aggressively compressed to protect quality.
  • The payloads got drastically smaller and LCP got quicker, at almost no creative risk.
// Example shape used across components
export function BrandLogo() {
  return (
    <picture>
      <source srcSet="/images/logo.webp" type="image/webp" />
      <img
        src="/images/logo.png"
        width="160"
        height="48"
        alt="Brand"
        loading="lazy"
        decoding="async"
      />
    </picture>
  );
}

2) Loading and CSS

  • Removed render-blocking CSS; inlined only critical styles and deferred the rest.
  • Tightened preconnect/preload hints to match real assets.
  • Swapped "transition-all" for targeted transitions to reduce forced reflows.

3) Content and pipeline hygiene

  • Hardened the MDX/blog pipeline so content renders the same locally and in prod.
  • Kept CV page and CV PDF in lockstep (a small build step prevents drift).
  • Added lightweight checks so broken links/image paths don't slip through.

4) Accessibility and SEO

  • Wrote meaningful alt text and clarified the semantics.
  • Cleaned up titles, descriptions, and canonicals, and got rid of duplicated headings.
  • Small changes, but Lighthouse and the search previews improved fast.

Hardest problem: Mermaid visuals

Front-end visuals were where AI struggled most, especially Mermaid styling, sizing, and theming. What worked:

  • Treat diagrams as responsive components (container-width, not fixed px).
  • Use Mermaid's built-in theming for dark mode instead of ad-hoc CSS.
  • Ensure fonts load predictably so label sizes don't jump between devices.
  • Avoid position: absolute hacks; give diagrams a real layout context.
// components/Mermaid.tsx
'use client';
import { useEffect } from 'react';
import mermaid from 'mermaid';

export default function Mermaid({ chart }: { chart: string }) {
  useEffect(() => {
    mermaid.initialize({
      startOnLoad: true,
      theme: 'default',
      securityLevel: 'loose',
      fontFamily: 'ui-sans-serif, system-ui',
    });
  }, []);
  return <div className="mermaid max-w-full overflow-auto">{chart}</div>;
}

My real loop (because it's a personal site)

I ran an extreme programming cycle that I would not use in enterprise apps: prompt → push to main → test in prod. It kept momentum high and the feedback real (because prod is the only truth that matters for a personal site).

Diagram 1 as text

9 steps, 1 of them a decision. Starts at “Spot issue on site”.

At work I'd use a safer loop: local run, tests, preview deployments. For a personal site, this speed was fun and productive.

Guardrails that actually helped

  • Unit tests as "intent beacons". I add or update tests around the area I'm changing, so the AI has a target and I have a safety net.
  • Do-not-touch zones. Config, secrets, and infra are off-limits unless I say otherwise.
  • Small diffs with measurable outcomes. If I can't write a one-line "why" in the commit, it's probably too big.
// Example: make CLS improvements explicit
import { render } from '@testing-library/react';
import Home from '@/app/page';

it('renders hero image with fixed dimensions to avoid layout shift', () => {
  const { getByAltText } = render(<Home />);
  const img = getByAltText(/hero/i) as HTMLImageElement;
  expect(img.width).toBeGreaterThan(0);
  expect(img.height).toBeGreaterThan(0);
  expect(img.getAttribute('loading')).toBe('lazy');
});

The one thing I'd tell someone starting out

If you're stuck in a prompt loop, it's almost always one of two things:

  1. Context. You haven't given it enough specific code, constraints, or failure output.
  2. Model. You've given it everything you have, and it's time to try a different one.

Swapping the model broke several deadlocks for me. Once the context is maxed out, a different brain is the only variable left.

Written by Sam Ainsworth.

my face
© Sam Ainsworth 2024 - 2026. All Rights Reserved.privacy