DEV Community

Chilam Chan
Chilam Chan

Posted on

前端性能优化:15个工具页面首屏加载<2秒实践

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>
  } 
/>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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"
  />
Enter fullscreen mode Exit fullscreen mode
  • 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.js with date-fns: moment is ~240KB minified; date-fns is tree-shakable, so I only imported the 2-3 functions I needed (total ~10KB).
  • Ditched lodash for native methods: _.debounce? Replaced with a 10-line custom debounce function. _.cloneDeep? Used JSON.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 }
    })
  ]
});
Enter fullscreen mode Exit fullscreen mode

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)