Optimizing FCP (First Contentful Paint)
FCP — time until first visible content appears: text, image, SVG, canvas. Good value: ≤ 1.8 seconds. FCP directly affects perceived speed — user sees something happening.
Difference FCP vs LCP
- FCP — first content pixel (any)
- LCP — largest element
Poor FCP almost always means poor LCP. Good FCP doesn't guarantee good LCP.
Render-blocking resources — main cause of poor FCP
Browser stops rendering on every <link> CSS and <script> in <head>.
<!-- Bad -->
<head>
<link rel="stylesheet" href="/css/app.css"> <!-- blocks -->
<link rel="stylesheet" href="/css/plugins.css"> <!-- blocks -->
<script src="/js/analytics.js"></script> <!-- blocks -->
</head>
<!-- Good -->
<head>
<!-- Critical CSS inline -->
<style>
body { margin: 0; font-family: system-ui, sans-serif; }
.header { height: 64px; background: #fff; }
/* ... minimal CSS for above-the-fold content ... */
</style>
<!-- Non-critical CSS async -->
<link rel="preload" href="/css/app.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<!-- Scripts — defer or async -->
<script defer src="/js/app.js"></script>
<script async src="/js/analytics.js"></script>
</head>
Critical CSS — automatic generation
// vite.config.ts with critters plugin
import { defineConfig } from 'vite';
import { critters } from 'critters';
export default defineConfig({
plugins: [
critters({
preload: 'swap', // preload non-critical styles
pruneSource: false, // don't delete CSS from source file
})
]
});
For Laravel — spatie/laravel-vite-plugin package + Nginx config for pre-built page serving.
Server-Side Rendering for React
SPA has poor FCP — user sees blank white screen until JS executes.
// Next.js — SSR out of the box
// Inertia.js — SSR for Laravel
// vite.config.ts for Inertia SSR
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
laravel({ input: 'resources/js/app.tsx', ssr: 'resources/js/ssr.tsx' }),
react(),
],
});
SSR improves FCP by 0.5–2 seconds for slow connections and devices.
Fonts
/* System stack — zero delay */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
/* If custom font needed — preload + font-display: swap */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-regular.woff2') format('woff2');
font-display: swap; /* show fallback immediately, replace when loaded */
unicode-range: U+0400-045F, U+0490-0491; /* only Cyrillic */
}
<link rel="preload" href="/fonts/inter-regular.woff2"
as="font" type="font/woff2" crossorigin>
Measurement
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
console.log('FCP:', entry.startTime);
}
}
}).observe({ type: 'paint', buffered: true });
Quick FCP checklist
- Inline critical CSS (above-the-fold styles)
- All external styles — async via
preload - Scripts —
deferorasync - Font — preload +
font-display: swap - TTFB < 600 ms (server cache)
- SSR enabled (for React/Vue SPA)
- Removed unused CSS rules (PurgeCSS/Tailwind purge)
Optimization time: 1–2 days for critical CSS setup and render-blocking resources.







