Performance 13 min read

Common Frontend Mistakes That Hurt Website Performance

The performance-killing mistakes I fix on almost every client audit — from render-blocking scripts to unoptimized images and JavaScript bloat that tanks Core Web Vitals.

By Omprakash Tanwar
Developer reviewing website performance issues on multiple screens

Every performance audit I run for a new client surfaces the same categories of problems. The stack changes — WordPress, React SPA, Next.js, Webflow export — but the mistakes rhyme. Render-blocking resources. Unoptimized images. JavaScript that ships before the user can read a headline. Third-party scripts that hijack the main thread.

I’m not writing this to shame anyone. I’ve made most of these mistakes myself early in my career. But after 30+ client audits as a freelance frontend developer, I can predict what’s slowing a site down before I open DevTools. This article catalogs the mistakes I fix most often, why they hurt, and the specific corrections that move Core Web Vitals from red to green.

Mistake 1: Shipping JavaScript the Page Doesn’t Need

The most widespread performance problem on the modern web is simple: pages that display content are built like applications that manage state.

A law firm’s “About Us” page doesn’t need 280 KB of React runtime to display three paragraphs and a team photo. A restaurant’s menu page doesn’t need a client-side router to show appetizers and entrees. Yet I regularly audit marketing sites shipping 150–400 KB of JavaScript for pages with zero interactivity beyond a mobile menu.

Why it hurts: JavaScript must be downloaded, parsed, compiled, and executed before the browser can fully render the page. On a mid-range Android phone over 4G, 300 KB of JS adds 1–3 seconds before the page is interactive. That directly impacts INP and time-to-interactive.

The fix: Match your architecture to your content.

<!-- Astro: static HTML, JS only for the mobile menu -->
<header>
  <nav><!-- static links --></nav>
  <MobileMenu client:media="(max-width: 768px)" />
</header>
// Next.js: Server Component for content, Client Component for interactivity
// page.tsx — Server Component
export default async function AboutPage() {
  const team = await getTeamMembers();
  return (
    <div>
      <h1>About Us</h1>
      <TeamGrid members={team} />
    </div>
  );
}

How to detect it: Open Chrome DevTools → Network → filter by JS. If your content page transfers more than 50 KB of JavaScript, question every kilobyte.

Mistake 2: Unoptimized Images

Images are the LCP element on 70%+ of web pages. I still find hero images that are 3 MB PNGs, product photos served at 4000px width on mobile, and <img> tags with no width, height, or loading attributes.

Why it hurts: A 3 MB hero image on 4G takes 4–8 seconds to download. Without explicit dimensions, the browser can’t reserve space, causing CLS as the image loads and pushes content down.

The fix:

// Next.js
import Image from "next/image";

<Image
  src="/hero.webp"
  alt="Modern office workspace with natural lighting"
  width={1200}
  height={630}
  priority
  sizes="(max-width: 768px) 100vw, 1200px"
/>
---
import { Image } from "astro:assets";
import hero from "../assets/hero.webp";
---

<Image
  src={hero}
  alt="Modern office workspace with natural lighting"
  widths={[640, 1024, 1200]}
  sizes="(max-width: 768px) 100vw, 1200px"
  loading="eager"
/>

Rules:

  • Convert to WebP or AVIF — 60–80% smaller than PNG/JPEG
  • Serve responsive sizes — don’t send 2000px images to 400px screens
  • Set explicit dimensions — prevents CLS
  • priority / loading="eager" for LCP images only
  • loading="lazy" for everything below the fold

How to detect it: Lighthouse flags “Properly size images” and “Serve images in next-gen formats.” Check the LCP element in the Performance panel.

Mistake 3: Render-Blocking Fonts

Teams load four font weights from Google Fonts CDN via a CSS @import, blocking rendering until the font CSS and files download.

Why it hurts: The browser can’t render text in your brand font until the font file downloads. If the font fails or is slow, you get invisible text (FOIT) or layout shift when a fallback swaps in (FOUT). Both hurt LCP and CLS.

The fix:

// Next.js — self-hosted, zero layout shift
import { Inter } from "next/font/google";

const inter = Inter({
  subsets: ["latin"],
  display: "swap",
  weight: ["400", "600"],
});
/* Self-hosted with matched fallback */
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-var.woff2") format("woff2");
  font-display: swap;
}

body {
  font-family: "Inter", system-ui, -apple-system, sans-serif;
}

Limit to 2–3 weights. Preload the primary weight. Use font-display: swap. Match fallback font metrics to minimize CLS.

How to detect it: Network tab shows font requests blocking render. Lighthouse flags “Ensure text remains visible during webfont load.”

Mistake 4: Third-Party Script Bloat

The average client site I audit loads 5–12 third-party scripts: Google Analytics, Facebook Pixel, HubSpot, Intercom, Hotjar, Google Tag Manager (which loads even more scripts), a chat widget, and a cookie consent banner that loads its own dependencies.

Why it hurts: Each script competes for main thread time. Chat widgets alone routinely add 150–300ms to INP. GTM can inject scripts you didn’t explicitly approve. On mobile, third-party JavaScript is often the single largest INP contributor.

The fix:

import Script from "next/script";

{/* Analytics — after page is interactive */}
<Script
  src="https://www.googletagmanager.com/gtag/js?id=G-XXX"
  strategy="afterInteractive"
/>

{/* Chat — only when idle, only on relevant pages */}
{isContactPage && (
  <Script
    src="https://widget.intercom.io/widget/xxx"
    strategy="lazyOnload"
  />
)}

Audit process:

  1. List every third-party script in Network tab
  2. Ask the client: “Do you actively use the data from this tool?”
  3. Remove unused scripts
  4. Defer remaining scripts to lazyOnload
  5. Gate page-specific scripts (chat on contact pages only)

How to detect it: Chrome DevTools → Performance → record a page load → Bottom-Up view → sort by Self Time → look for non-your-domain scripts.

Mistake 5: Layout Shift From Missing Dimensions

Images without width and height. Ads that inject content. Cookie banners that push the page down. Dynamic content inserted above existing content. Web fonts that swap with different metrics.

Why it hurts: CLS directly measures user frustration. A button shifting position as someone tries to tap it causes misclicks and abandoned actions. Google uses CLS as a ranking signal.

The fix:

<!-- Always reserve space -->
<img src="product.jpg" alt="Blue mug" width="400" height="400" />

<div class="ad-slot" style="min-height: 250px;">
  <!-- ad loads here without shifting content below -->
</div>
/* Fixed-position cookie banner doesn't push content */
.cookie-banner {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 50;
}

How to detect it: Lighthouse CLS audit. Chrome DevTools → Experience section in Performance recording shows layout shift events with sources.

Mistake 6: No Caching Strategy

Dynamic server rendering for pages that change once a day. API responses with no Cache-Control headers. Static assets without long-lived cache headers. Every visit triggers a full server round-trip.

Why it hurts: TTFB directly impacts LCP. If your server takes 800ms to render a marketing page that hasn’t changed in a week, you’re wasting 800ms on every visit.

The fix:

// Next.js ISR for pages that change periodically
export const revalidate = 3600;

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug);
  return <article>{post.content}</article>;
}
// next.config.js — cache static assets for 1 year
headers: async () => [
  {
    source: "/:all*(svg|jpg|png|webp|woff2|js|css)",
    headers: [
      {
        key: "Cache-Control",
        value: "public, max-age=31536000, immutable",
      },
    ],
  },
],

For Astro sites, the default static output caches perfectly on any CDN. Set cache headers at the CDN level.

How to detect it: Check response headers in Network tab. TTFB over 600ms on content pages is a red flag.

Mistake 7: CSS and JavaScript Not Minified or Compressed

In 2026, this is less common on modern frameworks that minify at build time. But I still find it on WordPress sites with 15 plugins, hand-coded HTML sites, and projects where the build pipeline was never configured.

Why it hurts: Unminified CSS/JS can be 3–5x larger than necessary. Without Brotli/gzip compression, transfer sizes balloon.

The fix:

  • Ensure your build tool minifies (Vite, Next.js, and Astro do this by default)
  • Verify your host serves Brotli compression (Cloudflare, Netlify, Vercel do by default)
  • Remove unused CSS — Tailwind purges automatically; audit custom CSS manually
  • Don’t @import multiple CSS files in production — bundle them

How to detect it: View page source. If CSS and JS are human-readable with comments and whitespace in production, minification isn’t working.

Mistake 8: Synchronous Layout Thrashing in JavaScript

Reading DOM dimensions then writing styles in a loop forces the browser to recalculate layout on every iteration.

Why it hurts: Each read-write cycle triggers a forced reflow. In a loop of 100 items, that’s 100 reflows. Main thread blocked. INP spikes.

The fix:

// Bad — interleaved reads and writes cause reflows
elements.forEach((el) => {
  const height = el.offsetHeight; // READ — forces reflow
  el.style.height = height + 10 + "px"; // WRITE
});

// Good — batch reads, then batch writes
const heights = elements.map((el) => el.offsetHeight); // READ batch
elements.forEach((el, i) => {
  el.style.height = heights[i] + 10 + "px"; // WRITE batch
});

Better yet, use CSS for layout changes instead of JavaScript:

.expandable {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.3s ease;
}

.expandable.open {
  grid-template-rows: 1fr;
}

How to detect it: Chrome Performance panel shows long purple “Layout” blocks. React DevTools Profiler shows components with high render times caused by DOM measurements.

Mistake 9: Over-Fetching and Waterfall Requests

Pages that make 5 sequential API calls before rendering content. GraphQL queries that fetch every field when the page needs three. Client-side useEffect chains that fetch user, then posts, then comments, then recommendations — each waiting for the previous.

Why it hurts: Waterfall requests multiply latency. Four sequential 200ms API calls = 800ms minimum before content appears. Parallel calls = 200ms.

The fix:

// Bad — sequential waterfall
const user = await getUser();
const posts = await getPosts(user.id);
const comments = await getComments(user.id);

// Good — parallel
const [user, posts, comments] = await Promise.all([
  getUser(),
  getPosts(),
  getComments(),
]);
// Server Component — data fetched before HTML is sent
export default async function DashboardPage() {
  const [stats, activities, notifications] = await Promise.all([
    getStats(),
    getActivities(),
    getNotifications(),
  ]);

  return <Dashboard stats={stats} activities={activities} notifications={notifications} />;
}

For GraphQL, fetch only the fields the page needs. Use DataLoader or query batching for related entities.

How to detect it: Network tab waterfall view. Look for requests that start only after a previous request completes.

Mistake 10: Ignoring Mobile and Slow Networks

Developers optimize on M-series MacBooks with fiber internet, then wonder why CrUX data shows failing vitals. Mobile devices have less CPU, less memory, and often 4G connections with 150ms+ latency.

Why it hurts: Over 60% of web traffic is mobile. Google measures Core Web Vitals from real Chrome mobile users. Your dev machine is not your user.

The fix:

  • Test with Chrome DevTools throttling: 4G + 4x CPU slowdown
  • Set performance budgets based on mobile, not desktop
  • Test on a real mid-range Android device monthly
  • Monitor CrUX field data, not just Lighthouse
# Lighthouse with mobile throttling
npx lighthouse https://yoursite.com --preset=mobile --throttling.cpuSlowdownMultiplier=4

How to detect it: Compare Lighthouse mobile vs desktop scores. A 40-point gap means you have mobile-specific problems.

Mistake 11: Animating the Wrong Properties

Animating width, height, top, left, and margin triggers layout recalculation on every frame. Smooth on your dev machine. Janky on a budget phone.

Why it hurts: Layout-triggering animations run on the main thread at 60fps. A 300ms animation with layout thrashing can block interactions, hurting INP.

The fix:

/* Bad — triggers layout */
.panel {
  transition: height 0.3s ease;
}

/* Good — GPU-composited */
.panel {
  transform: scaleY(0);
  transform-origin: top;
  transition: transform 0.3s ease;
}

.panel.open {
  transform: scaleY(1);
}

/* Best for simple fades */
.fade-in {
  opacity: 0;
  transition: opacity 0.3s ease;
}

.fade-in.visible {
  opacity: 1;
}

Only animate transform and opacity for 60fps performance. Use will-change sparingly and remove it after animation completes.

How to detect it: Chrome Performance panel → Frames section. Red bars indicate dropped frames. Check which CSS properties are triggering “Layout” or “Paint” events.

Mistake 12: No Performance Monitoring Post-Launch

Teams optimize before launch, hit Lighthouse 90+, ship, and never measure again. Then a marketing manager adds a HubSpot embed, a developer imports a charting library for one admin widget, and performance degrades 30% over three months without anyone noticing.

Why it hurts: Performance is not a one-time task. Every new feature, script, and dependency is a potential regression.

The fix:

# Lighthouse CI on every PR
- uses: treosh/lighthouse-ci-action@v11
  with:
    budgetPath: ./lighthouse-budget.json
// Real-user monitoring in production
import { onLCP, onINP, onCLS } from "web-vitals";

onLCP((metric) => reportToAnalytics(metric));
onINP((metric) => reportToAnalytics(metric));
onCLS((metric) => reportToAnalytics(metric));

Set a monthly calendar reminder to check Search Console Core Web Vitals. Review bundle size after every sprint.

The Audit Process I Run for Clients

When a client hires me for a performance audit, here’s my systematic process:

Hour 1: Lighthouse mobile + desktop, CrUX data, Network tab waterfall, bundle size inventory.

Hour 2: Identify LCP element, worst INP interactions, CLS sources. Screenshot and document each.

Hour 3: Prioritize fixes by impact vs effort. Quick wins first (image optimization, font loading, script deferral).

Hours 4–8: Implement fixes, re-measure after each major change.

Deliverable: Before/after metrics, list of changes made, performance budget recommendations, and monitoring setup.

Most audits find 8–15 fixable issues. The top 3 usually account for 70% of the improvement.

Conclusion

These mistakes aren’t exotic edge cases. They’re the default state of websites built under deadline pressure without performance as a requirement. The good news is they’re fixable — often without a rewrite. Optimize your images, reduce JavaScript, defer third-party scripts, set caching, and measure on real devices.

Performance isn’t a feature you add later. It’s a constraint you design within from the start. But even if you’re fixing a slow site after launch, these 12 mistakes are where I’d look first. They’ve never steered me wrong across 30+ client audits.

Key Takeaways

  • The biggest performance mistake is shipping application-level JavaScript for content-level pages
  • Unoptimized images are the most common LCP failure — use WebP/AVIF, responsive sizes, and explicit dimensions
  • Third-party scripts (chat, analytics, GTM) are often the biggest INP contributor — defer and gate them
  • Render-blocking fonts hurt LCP and CLS — self-host with font-display: swap and limit weights
  • Missing image dimensions and dynamic content injection cause CLS — always reserve space
  • Waterfall API requests multiply latency — use Promise.all and Server Components for parallel fetching
  • Test on throttled mobile, not just your dev machine — CrUX data comes from real mobile users
  • Animate only transform and opacity for GPU-composited 60fps animations
  • Set up Lighthouse CI and RUM monitoring to catch regressions after launch
  • Most audits find 8–15 fixable issues where the top 3 account for 70% of the improvement
Table of Contents