Frontend Performance Optimization: Getting 15 Tool Pages to Load in <2 Seconds
Hey folks! I’m the solo dev behind TinyUtils – a suite of 15 free, no-signup, 100% in-browser utilities (think JSON formatters, password generators, unit converters). Since users come for quick tasks, performance isn’t just a nice-to-have – it’s the product. A slow load means they’ll bounce before even using the tool.
A few months back, my Lighthouse scores were… rough. Initial load times on 4G hovered around 4-5 seconds, and that was with only 10 tools. Adding 5 more? I knew I needed to get serious about optimization. Today, all 15 tools load in under 2 seconds on 4G, and I want to break down how I did it – no fluff, just concrete steps that worked for my stack (React + Vite, static hosting).
1. Code Splitting: Stop Shipping All 15 Tools at Once
The biggest mistake? Bundling all tool logic into a single main.js. Each tool has unique dependencies (e.g., date-fns for the timestamp converter, marked for the markdown previewer) – why make users download code for tools they’re not using?
Fix: Dynamic imports with React.lazy.
I refactored the app to load tool components only when needed. The main page (tool selector) is lightweight, and tools are fetched on demand. Here’s a simplified snippet:
// Before: Importing all tools upfront
import JSONFormatter from './tools/JSONFormatter';
import PasswordGenerator from './tools/PasswordGenerator';
// ... 13 more imports
// After: Lazy-loading tools
const JSONFormatter = React.lazy(() => import('./tools/JSONFormatter'));
const PasswordGenerator = React.lazy(() => import('./tools/PasswordGenerator'));
// In the router (React Router)
<Route
path="/json-formatter"
element={
<Suspense fallback={<LoadingSpinner />}>
<JSONFormatter />
</Suspense>
}
/>
Result: Initial bundle size dropped from ~800KB to ~220KB (unminified). No more waiting for unused code!
2. Lazy-Load Non-Critical UI (Even the "Small" Stuff)
Not all content on a tool page is critical. Things like help text, examples, or secondary features can wait until the user scrolls near them.
Fix: Intersection Observer for non-essential elements.
For example, the "Usage Tips" section at the bottom of each tool page? I wrap it in a component that only renders when it’s 200px from the viewport:
function LazyLoad({ children }) {
const [isVisible, setIsVisible] = useState(false);
const ref = useRef();
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => setIsVisible(entry.isIntersecting),
{ rootMargin: '200px' } // Load 200px before it's visible
);
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return isVisible ? <div ref={ref}>{children}</div> : null;
}
// Usage in a tool page:
<LazyLoad>
<div className="usage-tips">
<h3>Pro Tips</h3>
{/* ... */}
</div>
</LazyLoad>
This kept initial render times snappy – the browser isn’t bogged down rendering offscreen content.
3. Image Optimization: Every KB Counts
TinyUtils uses icons (for tool categories) and a few screenshots (e.g., "how to use" examples). Even small images add up if unoptimized.
Fixes:
- WebP/AVIF over PNG/JPG: Converted all icons to WebP (using Squoosh) – average size reduction of ~60% (e.g., a 40KB PNG icon became 15KB WebP).
-
Responsive images with
srcset: For screenshots, served smaller versions to mobile users:
<img
src="/screenshots/json-formatter-sm.webp"
srcset="/screenshots/json-formatter-sm.webp 400w, /screenshots/json-formatter-lg.webp 800w"
alt="JSON Formatter example"
/>
- SVG for simple icons: Replaced some raster icons with inline SVGs (no network request, and infinitely scalable).
4. Trim Dependencies: Less is More
Third-party libraries are convenient, but they’re often bloated with features I don’t need. I audited package.json and axed bloat:
-
Replaced
moment.jswithdate-fns:momentis ~240KB minified;date-fnsis tree-shakable, so I only imported the 2-3 functions I needed (total ~10KB). -
Ditched
lodashfor native methods:_.debounce? Replaced with a 10-line custom debounce function._.cloneDeep? UsedJSON.parse(JSON.stringify())for simple objects (good enough for my use case). - Inlined small utilities: Instead of installing a library for "slugify" or "capitalize", wrote 2-line helpers.
Result: Bundle size dropped another ~150KB.
5. Vite Build Optimizations
I switched from Create React App to Vite early on, but I wasn’t leveraging its full power. Tweaking the Vite config squeezed out more gains:
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
// Split chunks for shared code (e.g., React, common utilities)
manualChunks: {
react: ['react', 'react-dom'],
utils: ['date-fns', 'marked'] // Shared deps across tools
}
}
},
// Enable brotli compression (smaller than gzip)
assetsDir: 'assets',
compress: true
},
plugins: [
// Auto-optimize images during build
viteImagemin({
gifsicle: { optimizationLevel: 7 },
webp: { quality: 80 }
})
]
});
Vite’s esbuild-based minification and tree-shaking also helped – my final JS/CSS assets are minified and compressed (brotli), so even the 220KB initial bundle becomes ~60KB over the wire.
The Result: <2s Loads (Finally!)
After these changes, Lighthouse performance scores went from "Poor" (50s) to "Good" (90s). On 4G, first contentful paint (FCP) is ~1.2s, and time to interactive (TTI) is ~1.8s – well under the 2s goal.
More importantly, users notice. I’ve had a few emails saying, "This site is so fast compared to others!" – and that’s the best metric.
Key Takeaways
If you’re building a multi-tool or content-heavy site, here’s what worked for me:
- Split code aggressively: Load only what the user needs now.
- Lazy-load everything non-critical: Use Intersection Observer for offscreen content.
-
Optimize images like your life depends on it: WebP,
srcset, and SVG are your friends. - Audit dependencies: Most apps can trim 20-30% of bundle size by ditching bloat.
Performance is a never-ending battle, but these steps got me from "sluggish" to "snappy" with minimal effort. What’s your go-to performance win? Let me know in the comments!
(And if you need a quick utility, check out TinyUtils – it won’t make you wait.) 😊
Top comments (0)