<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Chilam Chan</title>
    <description>The latest articles on DEV Community by Chilam Chan (@chilam_chan_68a518a731328).</description>
    <link>https://dev.to/chilam_chan_68a518a731328</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4126468%2F43aa359e-1b8b-4f2d-bc35-ed6beca8920a.png</url>
      <title>DEV Community: Chilam Chan</title>
      <link>https://dev.to/chilam_chan_68a518a731328</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chilam_chan_68a518a731328"/>
    <language>en</language>
    <item>
      <title>前端性能优化:15个工具页面首屏加载&lt;2秒实践</title>
      <dc:creator>Chilam Chan</dc:creator>
      <pubDate>Tue, 29 Sep 2026 01:12:51 +0000</pubDate>
      <link>https://dev.to/chilam_chan_68a518a731328/qian-duan-xing-neng-you-hua-15ge-gong-ju-ye-mian-shou-ping-jia-zai-2miao-shi-jian-e72</link>
      <guid>https://dev.to/chilam_chan_68a518a731328/qian-duan-xing-neng-you-hua-15ge-gong-ju-ye-mian-shou-ping-jia-zai-2miao-shi-jian-e72</guid>
      <description>&lt;h1&gt;
  
  
  Frontend Performance Optimization: Getting 15 Tool Pages to Load in &amp;lt;2 Seconds
&lt;/h1&gt;

&lt;p&gt;Hey folks! I’m the solo dev behind &lt;a href="https://boring-tools-6ip.pages.dev" rel="noopener noreferrer"&gt;TinyUtils&lt;/a&gt; – 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.  &lt;/p&gt;

&lt;p&gt;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).  &lt;/p&gt;

&lt;h2&gt;
  
  
  1. Code Splitting: Stop Shipping All 15 Tools at Once
&lt;/h2&gt;

&lt;p&gt;The biggest mistake? Bundling &lt;em&gt;all&lt;/em&gt; tool logic into a single &lt;code&gt;main.js&lt;/code&gt;. Each tool has unique dependencies (e.g., &lt;code&gt;date-fns&lt;/code&gt; for the timestamp converter, &lt;code&gt;marked&lt;/code&gt; for the markdown previewer) – why make users download code for tools they’re not using?  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Dynamic imports with React.lazy.  &lt;/p&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: Importing all tools upfront&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;JSONFormatter&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./tools/JSONFormatter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;PasswordGenerator&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./tools/PasswordGenerator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ... 13 more imports&lt;/span&gt;

&lt;span class="c1"&gt;// After: Lazy-loading tools&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;JSONFormatter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./tools/JSONFormatter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PasswordGenerator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./tools/PasswordGenerator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// In the router (React Router)&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Route&lt;/span&gt; 
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/json-formatter"&lt;/span&gt; 
  &lt;span class="na"&gt;element&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LoadingSpinner&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;JSONFormatter&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;}&lt;/span&gt; 
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: Initial bundle size dropped from ~800KB to ~220KB (unminified). No more waiting for unused code!  &lt;/p&gt;

&lt;h2&gt;
  
  
  2. Lazy-Load Non-Critical UI (Even the "Small" Stuff)
&lt;/h2&gt;

&lt;p&gt;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.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Intersection Observer for non-essential elements.  &lt;/p&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LazyLoad&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isVisible&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsVisible&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setIsVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rootMargin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;200px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// Load 200px before it's visible&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;isVisible&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage in a tool page:&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LazyLoad&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"usage-tips"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Pro Tips&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;LazyLoad&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kept initial render times snappy – the browser isn’t bogged down rendering offscreen content.  &lt;/p&gt;

&lt;h2&gt;
  
  
  3. Image Optimization: Every KB Counts
&lt;/h2&gt;

&lt;p&gt;TinyUtils uses icons (for tool categories) and a few screenshots (e.g., "how to use" examples). Even small images add up if unoptimized.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixes:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebP/AVIF over PNG/JPG:&lt;/strong&gt; Converted all icons to WebP (using &lt;a href="https://squoosh.app/" rel="noopener noreferrer"&gt;Squoosh&lt;/a&gt;) – average size reduction of ~60% (e.g., a 40KB PNG icon became 15KB WebP).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Responsive images with &lt;code&gt;srcset&lt;/code&gt;:&lt;/strong&gt; For screenshots, served smaller versions to mobile users:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; 
    &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/screenshots/json-formatter-sm.webp"&lt;/span&gt; 
    &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"/screenshots/json-formatter-sm.webp 400w, /screenshots/json-formatter-lg.webp 800w"&lt;/span&gt; 
    &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"JSON Formatter example"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SVG for simple icons:&lt;/strong&gt; Replaced some raster icons with inline SVGs (no network request, and infinitely scalable).
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Trim Dependencies: Less is More
&lt;/h2&gt;

&lt;p&gt;Third-party libraries are convenient, but they’re often bloated with features I don’t need. I audited &lt;code&gt;package.json&lt;/code&gt; and axed bloat:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Replaced &lt;code&gt;moment.js&lt;/code&gt; with &lt;code&gt;date-fns&lt;/code&gt;:&lt;/strong&gt; &lt;code&gt;moment&lt;/code&gt; is ~240KB minified; &lt;code&gt;date-fns&lt;/code&gt; is tree-shakable, so I only imported the 2-3 functions I needed (total ~10KB).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ditched &lt;code&gt;lodash&lt;/code&gt; for native methods:&lt;/strong&gt; &lt;code&gt;_.debounce&lt;/code&gt;? Replaced with a 10-line custom debounce function. &lt;code&gt;_.cloneDeep&lt;/code&gt;? Used &lt;code&gt;JSON.parse(JSON.stringify())&lt;/code&gt; for simple objects (good enough for my use case).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inlined small utilities:&lt;/strong&gt; Instead of installing a library for "slugify" or "capitalize", wrote 2-line helpers.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: Bundle size dropped another ~150KB.  &lt;/p&gt;

&lt;h2&gt;
  
  
  5. Vite Build Optimizations
&lt;/h2&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;rollupOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Split chunks for shared code (e.g., React, common utilities)&lt;/span&gt;
        &lt;span class="na"&gt;manualChunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;react&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;utils&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;date-fns&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;marked&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;// Shared deps across tools&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// Enable brotli compression (smaller than gzip)&lt;/span&gt;
    &lt;span class="na"&gt;assetsDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// Auto-optimize images during build&lt;/span&gt;
    &lt;span class="nf"&gt;viteImagemin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;gifsicle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;optimizationLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;webp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vite’s esbuild-based minification and tree-shaking also helped – my final JS/CSS assets are minified &lt;em&gt;and&lt;/em&gt; compressed (brotli), so even the 220KB initial bundle becomes ~60KB over the wire.  &lt;/p&gt;

&lt;h2&gt;
  
  
  The Result: &amp;lt;2s Loads (Finally!)
&lt;/h2&gt;

&lt;p&gt;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.  &lt;/p&gt;

&lt;p&gt;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.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;If you’re building a multi-tool or content-heavy site, here’s what worked for me:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Split code aggressively:&lt;/strong&gt; Load only what the user needs &lt;em&gt;now&lt;/em&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lazy-load everything non-critical:&lt;/strong&gt; Use Intersection Observer for offscreen content.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimize images like your life depends on it:&lt;/strong&gt; WebP, &lt;code&gt;srcset&lt;/code&gt;, and SVG are your friends.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit dependencies:&lt;/strong&gt; Most apps can trim 20-30% of bundle size by ditching bloat.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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!  &lt;/p&gt;

&lt;p&gt;(And if you need a quick utility, check out &lt;a href="https://boring-tools-6ip.pages.dev" rel="noopener noreferrer"&gt;TinyUtils&lt;/a&gt; – it won’t make you wait.) 😊&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I built 15 browser tools with zero backend</title>
      <dc:creator>Chilam Chan</dc:creator>
      <pubDate>Wed, 16 Sep 2026 04:06:18 +0000</pubDate>
      <link>https://dev.to/chilam_chan_68a518a731328/how-i-built-15-browser-tools-with-zero-backend-4ffi</link>
      <guid>https://dev.to/chilam_chan_68a518a731328/how-i-built-15-browser-tools-with-zero-backend-4ffi</guid>
      <description>&lt;p&gt;Every "free online tool" I reached for wanted an upload, an account, or both. So I built 15 that need neither — and the constraint that made that possible turned out to be the best decision in the project: no backend at all.&lt;/p&gt;

&lt;p&gt;Here's what runs where.&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser can do more than we give it credit for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Image work (HEIC→JPG, compress, resize, strip EXIF): &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; + &lt;code&gt;createImageBitmap&lt;/code&gt;, and WASM (&lt;code&gt;heic2any&lt;/code&gt;) for formats the browser can't decode natively.&lt;/li&gt;
&lt;li&gt;PDF split/merge: &lt;code&gt;pdf-lib&lt;/code&gt;, entirely client-side.&lt;/li&gt;
&lt;li&gt;QR codes, password generation, JSON formatting, word counting: plain JS + &lt;code&gt;crypto.getRandomValues&lt;/code&gt; and &lt;code&gt;Intl.Segmenter&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing touches a server. "Your files never leave your device" isn't a privacy promise you have to trust — it's just how the thing works. Open DevTools → Network and watch: no request fires when you convert a file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why no-backend is a superpower for a solo dev
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$0 to run.&lt;/strong&gt; A zero-dependency static site generator emits plain HTML; Cloudflare Pages serves it free. 272 pages (15 tools × 16 languages + hub) cost nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing to operate.&lt;/strong&gt; No servers, no uploads to secure, no data to leak, no scaling. The whole "ops" surface is a git push.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's genuinely private&lt;/strong&gt;, which is a real differentiator when every competitor uploads your file to "process" it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The tradeoffs (being honest)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No accounts, no cross-device sync, no server-side history. For utilities, that's fine — you do one thing and leave.&lt;/li&gt;
&lt;li&gt;Some formats (TIFF, exotic codecs) the browser just won't decode; you draw the line and say so.&lt;/li&gt;
&lt;li&gt;SEO is the whole growth engine, since there's no viral loop. That's the part I'm still figuring out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're sitting on a "should this be a SaaS or just a tool" decision: a surprising amount can be &lt;em&gt;just a tool&lt;/em&gt;, and shipping it that way is faster, cheaper, and more private.&lt;/p&gt;

&lt;p&gt;Tools are here if useful: &lt;a href="https://boring-tools-6ip.pages.dev" rel="noopener noreferrer"&gt;https://boring-tools-6ip.pages.dev&lt;/a&gt; — happy to go deeper on any part of the stack in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>privacy</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Stop pasting sensitive JSON into random online formatters</title>
      <dc:creator>Chilam Chan</dc:creator>
      <pubDate>Tue, 15 Sep 2026 17:04:58 +0000</pubDate>
      <link>https://dev.to/chilam_chan_68a518a731328/stop-pasting-sensitive-json-into-random-online-formatters-26e1</link>
      <guid>https://dev.to/chilam_chan_68a518a731328/stop-pasting-sensitive-json-into-random-online-formatters-26e1</guid>
      <description>&lt;p&gt;Every developer has done it: you get an ugly one-line JSON blob back from an API, and you paste it into the first "JSON formatter online" result to make it readable.&lt;/p&gt;

&lt;p&gt;Here's the problem. Half the time that blob is a real response from &lt;em&gt;your&lt;/em&gt; system — it can contain access tokens, internal IDs, customer emails, signed URLs, session data. The moment you paste it into a random web tool, you've sent that data to a server you don't control. You don't know if it's logged, cached, or sitting in someone's analytics pipeline.&lt;/p&gt;

&lt;p&gt;For a side project, maybe you shrug. Inside a company with a compliance team, that's the kind of thing that ends up in an incident review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: format JSON in the browser, not on a server
&lt;/h2&gt;

&lt;p&gt;You don't need a server to pretty-print JSON. Everything — parsing, formatting, validating, minifying — can happen entirely client-side with &lt;code&gt;JSON.parse&lt;/code&gt; / &lt;code&gt;JSON.stringify&lt;/code&gt;. The data never has to leave the tab.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This is the whole "backend" of a JSON formatter:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pretty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A tool built this way can even work offline once the page has loaded, because there's nothing to call home to.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check if a tool is actually client-side
&lt;/h2&gt;

&lt;p&gt;Don't take a site's word for it. Open DevTools → Network tab, paste your JSON, hit format, and watch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No new request fires&lt;/strong&gt; → the work happened locally. Safe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A request goes out with your JSON in the payload&lt;/strong&gt; → your data just left the building.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do this once with any "online" dev tool that eats sensitive input (JSON, JWT decoders, "beautifiers", diff tools). You'll be surprised how many phone home.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I ended up using
&lt;/h2&gt;

&lt;p&gt;I got tired of doing that Network-tab check every time, so I built a JSON formatter that's 100% client-side — format, validate, minify, nothing uploaded, no signup:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://boring-tools-6ip.pages.dev/json-formatter/" rel="noopener noreferrer"&gt;https://boring-tools-6ip.pages.dev/json-formatter/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's part of a small set of no-upload browser utilities I'm building solo. If you deal with sensitive payloads regularly, the "does this leave my machine?" habit is worth building regardless of which tool you land on.&lt;/p&gt;

&lt;p&gt;What's your rule of thumb for pasting real data into third-party dev tools? Curious how others draw the line.&lt;/p&gt;

</description>
      <category>json</category>
      <category>webdev</category>
      <category>privacy</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
