DAILY NEWS

Stay Ahead, Stay Informed – Every Day

Advertisement
When (and when not) to inline images as Base64



Base64 image data URIs are one of those web techniques that look like a magic shortcut the first time you use them.

Instead of referencing an external file:

src=”/logo.png” alt=”Logo”>

Enter fullscreen mode

Exit fullscreen mode

you can put the image bytes directly in the document as text:

src=”data:image/png;base64,iVBORw0KGgoAAAANSUhEUg…” alt=”Logo”>

Enter fullscreen mode

Exit fullscreen mode

That can be useful. It can also make a page slower, harder to cache, and more annoying to maintain.

Here is the practical rule: inline images as Base64 when self-containment matters more than caching. Keep normal image files when the browser should be able to cache, resize, lazy-load, or optimize them independently.

What a Base64 image actually is

An image file is binary data. Base64 rewrites that binary data as plain text using a limited character set. To make the browser treat the text as an image, you wrap it in a data URI:

data:image/png;base64,iVBORw0KGgoAAAANSUhEUg…

Enter fullscreen mode

Exit fullscreen mode

The first part tells the browser the MIME type. The second part tells it the data is Base64 encoded. The long tail is the image itself.

Base64 is not compression. It is not encryption. It is just a text representation of the same bytes.

When inlining an image is worth it

1. Tiny icons and UI assets

For very small images, removing an extra HTTP request can be worth the extra bytes. This is especially true for small icons, logos, placeholders, simple UI sprites, or tiny transparent PNGs.

Modern HTTP/2 and HTTP/3 make extra requests cheaper than they used to be, so this is not an automatic win. But for a one-off tiny asset inside a small page or widget, a data URI can still be a clean choice.

2. Single-file deliverables

Sometimes the point is not raw page speed. Sometimes you need one file that carries everything with it:

an HTML report
an email template
a CodePen or demo snippet
a CMS block where you cannot upload assets
a test fixture that should not depend on external hosting

In those cases, Base64 is useful because the image travels with the HTML, CSS, JSON, or JavaScript.

3. Prototypes and throwaway snippets

If you are testing a layout, writing a bug reproduction, or pasting a minimal example into a ticket, a data URI can save time. You do not need to set up static hosting just to show one image.

4. Local-only conversion workflows

If the image is private, it is nice to avoid uploading it to a random converter. Browser APIs can generate a Base64 data URI locally, so the file never leaves your device.

When you should not inline the image

1. Large photos and hero images

Base64 usually makes the encoded data about 33% larger than the original binary file. That is because Base64 stores every 3 bytes as 4 text characters.

For a large JPG, PNG, or WebP, that extra size is not a rounding error. Keep big images as normal files.

2. Images reused across pages

An external image can be cached once and reused across page views. An inlined image is bundled into every document or stylesheet that contains it.

If the same logo appears on 20 pages, inlining it 20 times is usually worse than letting the browser cache one file.

3. Responsive images

Normal image files can use srcset, sizes, lazy loading, CDN transforms, format negotiation, and caching headers.

src=”/hero-800.webp”
srcset=”/hero-400.webp 400w, /hero-800.webp 800w, /hero-1600.webp 1600w”
sizes=”100vw”
loading=”lazy”
alt=”Product screenshot”
>

Enter fullscreen mode

Exit fullscreen mode

That is much harder to preserve when the image is baked into a string.

4. Anything you expect humans to edit

Base64 strings are unpleasant to review in Git diffs, easy to truncate by accident, and noisy inside templates. If designers, marketers, or other engineers need to update the image regularly, use a normal asset file.

How to generate a Base64 data URI in the browser

The simplest browser-native path is FileReader.readAsDataURL().

The result will look like this:

data:image/png;base64,iVBORw0KGgoAAAANSUhEUg…

Enter fullscreen mode

Exit fullscreen mode

You can use that string directly in HTML:

src=”data:image/png;base64,iVBORw0KGgoAAAANSUhEUg…” alt=”Logo”>

Enter fullscreen mode

Exit fullscreen mode

or in CSS:

.logo {
background-image: url(“data:image/png;base64,iVBORw0KGgoAAAANSUhEUg…”);
}

Enter fullscreen mode

Exit fullscreen mode

A simple checklist

Inline the image if:

it is small
it is not reused across many pages
self-contained delivery matters
you do not need responsive image behavior
the string will not make your source files painful to maintain

Keep it as a normal file if:

it is a photo or large graphic
it should be cached separately
it appears on many pages
it needs srcset, lazy loading, CDN resizing, or image optimization
non-developers need to replace it often

Tiny tool note

I built a small free tool for this workflow: PNG to Base64 converter. It runs entirely in the browser with FileReader, so the PNG is not uploaded, and it gives you the raw Base64 string plus ready-to-paste HTML and CSS snippets.

There is also a general image to Base64 converter for JPG, SVG, WebP, GIF, and other image formats.

Use Base64 as a packaging tool, not a default image strategy. When the image is tiny or the deliverable must be self-contained, it can be perfect. When performance, caching, and responsive delivery matter, boring old image files are still the better answer.



Source link

React useDebounce Hook: Debounce State & Callbacks (2026)



You have a search box. The user types react hooks, and your component fires an API request on every single keystroke — eleven requests for one query, ten of them already stale by the time they resolve. The fix everyone reaches for is debouncing: wait until the typing stops, then fire once. The fix everyone gets wrong is writing that debounce by hand with setTimeout inside a component, where stale closures, missing cleanup, and re-render churn quietly break it.

useDebounce is the hook that gets it right. This post covers the two shapes you actually need — debouncing a value and debouncing a callback — when to use each, and how to cancel or flush pending calls. Everything here is the real @reactuses/core API, SSR-safe and typed.

Why Not Just Use setTimeout?

Debouncing itself is simple: delay a function until a quiet period has passed, restarting the timer on every new call. (If you want the full conceptual breakdown — and how it differs from throttling — see Debounce vs Throttle in React.) The hard part is doing it inside a React component. Here is the naive version, and it has three bugs:

function Search() {
const (query, setQuery) = useState(”);
const timer = useRefReturnTypetypeof setTimeout>>();

function handleChange(e: React.ChangeEventHTMLInputElement>) {
const value = e.target.value;
setQuery(value);
clearTimeout(timer.current);
timer.current = setTimeout(() => {
fetchResults(value); // 🐛 see below
}, 300);
}

return input value={query} onChange={handleChange} />;
}

Enter fullscreen mode

Exit fullscreen mode

It leaks on unmount. If the component unmounts while a timer is pending, the callback still fires 300 ms later — often setting state on a gone component, or hitting an API for a screen the user already left.

It captures stale values. The moment you debounce anything other than the raw event value — a second piece of state, a prop, a derived value — the closure freezes whatever those were when the timer was set, not when it fires.

It spreads. Every place that needs debouncing re-implements the useRef + clearTimeout dance, and each copy is a chance to forget the cleanup.

A hook fixes all three in one place. ReactUse ships two, built on the battle-tested lodash.debounce internally so the edge cases (leading edge, max wait, trailing edge) are already handled.

useDebounce — Debounce a Value

The most common case: you have a value that changes rapidly and you want a second, lagging copy of it that only updates after things settle. That second copy is what you feed into expensive work.

import { useState, useEffect } from ‘react’;
import { useDebounce } from ‘@reactuses/core’;

function Search() {
const (query, setQuery) = useState(”);
const debouncedQuery = useDebounce(query, 300);

useEffect(() => {
if (!debouncedQuery) return;
fetchResults(debouncedQuery);
}, (debouncedQuery));

return (
input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder=”Search…”
/>
);
}

Enter fullscreen mode

Exit fullscreen mode

The signature is useDebounce(value, wait?, options?) and it returns the debounced value, with the same type as the input:

const debounced = useDebounce(value, 300);

Enter fullscreen mode

Exit fullscreen mode

The input (query) updates on every keystroke, so the controlled stays perfectly responsive — that’s the value you bind to the DOM. The output (debouncedQuery) only catches up 300 ms after the user stops typing, so it’s the value you put in the effect’s dependency array. The API fires once per pause instead of once per keystroke, and your input never feels laggy because the thing you typed into was never the thing being debounced.

This pattern — fast value for the UI, debounced value for the side effect — is the whole point. Keep them as two separate variables and the rest falls into place.

useDebounceFn — Debounce a Callback

Debouncing a value is great when the thing you want to throttle is state. But sometimes you want to debounce an action that takes arguments — an autosave, an analytics event, a resize handler — without routing it through state first. That’s useDebounceFn:

import { useDebounceFn } from ‘@reactuses/core’;

function Editor({ docId }: { docId: string }) {
const { run } = useDebounceFn((content: string) => {
saveDraft(docId, content);
}, 1000);

return (
textarea onChange={(e) => run(e.target.value)} />
);
}

Enter fullscreen mode

Exit fullscreen mode

useDebounceFn(fn, wait?, options?) returns an object with three members:

const { run, cancel, flush } = useDebounceFn(fn, 1000);

Enter fullscreen mode

Exit fullscreen mode

run — the debounced function. Call it as often as you like; fn only actually executes after the calls stop for wait ms. It forwards every argument through, so run(content) calls fn(content).

cancel — drop any pending invocation. Nothing fires.

flush — fire the pending invocation right now, instead of waiting out the timer.

Crucially, run always calls the latest version of your fn. Internally the hook keeps your callback in a ref, so even though the debounced wrapper is created once, it never goes stale — the docId closure problem from the setTimeout version simply doesn’t exist here. And the hook cancels any pending call automatically on unmount, so bug #1 is gone too.

useDebounce is actually built on top of useDebounceFn — it debounces a setState call and hands you the resulting value. Same engine, two ergonomics.

cancel and flush in practice

The cancel/flush pair is what raw setTimeout makes painful and a hook makes trivial. Two real cases:

function CommentBox() {
const { run: autosave, cancel, flush } = useDebounceFn(
(text: string) => saveDraft(text),
2000,
);

return (

textarea onChange={(e) => autosave(e.target.value)} />
{/* User hit “Post” — persist immediately, don’t wait out the 2s */}
button onClick={() => flush()}>Postbutton>
{/* User hit “Discard” — throw away the pending autosave */}
button onClick={() => cancel()}>Discardbutton>
>
);
}

Enter fullscreen mode

Exit fullscreen mode

flush guarantees the in-flight draft is written before the post request goes out; cancel makes sure a discarded draft doesn’t get saved a beat later. Both are one call.

Value or Callback — Which One?

A quick decision rule:

Reach for useDebounce when you’re debouncing a piece of state that something else reads — a search term, a filter, a slider value feeding a chart. You want a lagging value.
Reach for useDebounceFn when you’re debouncing an action with arguments — autosave, logging, firing a network request directly. You want a lagging function, plus cancel/flush control.

If you find yourself creating a piece of state only to debounce it and then immediately fire an effect, useDebounceFn is usually the more direct tool.

Tuning: leading, trailing, and maxWait

The optional third argument is passed straight through to lodash.debounce, so you get its full options object:

useDebounce(value, 300, {
leading: false, // don’t fire on the very first call (default)
trailing: true, // fire after the pause (default)
maxWait: 1000, // …but never wait longer than 1s total
});

Enter fullscreen mode

Exit fullscreen mode

Two knobs worth knowing:

leading: true fires on the first call immediately, then debounces the rest. Good for “respond instantly, then settle” interactions — the first click of a button feels snappy while rapid repeats are absorbed.

maxWait caps the total delay. With a pure trailing debounce, a user who types continuously for ten seconds gets zero updates until they stop. maxWait: 1000 forces an update at least once a second even mid-burst — the difference between a search box that feels alive and one that feels frozen.

SSR Safety

Both hooks are safe to render on the server. They touch no window, document, or browser timer during render — the debounced work only ever runs inside effects, which React never executes on the server. Drop them into a Next.js, Remix, or Astro component and there’s no typeof window guard to write, no hydration warning to chase. (If SSR-safety is a running theme in your codebase, SSR-Safe React Hooks goes deeper.)

The Rate-Limiting Family

useDebounce has three close relatives in ReactUse; pick by what you’re limiting and which shape you need:

The throttle pair mirrors the debounce pair exactly — same (value/fn, wait, options) signature, same return shapes — but enforces a steady cadence instead of waiting for silence. Use throttle for things that should update during a continuous gesture (scroll position, drag coordinates, a live progress readout); use debounce for things that should update only after it ends (search, autosave, validation). The full mental model is in Debounce vs Throttle in React: When to Use Which.

Takeaways

A hand-rolled setTimeout debounce inside a component ships three bugs by default: it leaks on unmount, it captures stale closures, and it gets copy-pasted.

useDebounce(value, wait) gives you a lagging copy of a value — type into the fast one, run effects off the slow one. Perfect for search-as-you-type.

useDebounceFn(fn, wait) debounces an action and hands you { run, cancel, flush }. run always calls your latest callback (no stale closures) and auto-cancels on unmount.
Use flush to commit a pending call early (submit) and cancel to drop it (discard).
The third argument is lodash.debounce options — leading for instant-first-call, maxWait to cap the delay so long bursts still update.
Both are SSR-safe and sit alongside useThrottle/useThrottleFn for the fixed-rate case.

Grab them from @reactuses/core and delete your clearTimeout boilerplate.



Source link

Plasmo vs CRXJS vs WXT: Which Chrome Extension Framework Should You Use in 2026?



If you are starting a browser extension in 2026, you no longer hand-roll a manifest.json, wire up a Webpack config, and pray that hot reload works on content scripts. Three projects now own this space: Plasmo, CRXJS, and WXT.

They look similar from the README, but they make very different bets. One is a full opinionated framework. One is just a build plugin. One tries to be the “Nuxt of extensions.” Pick wrong and you will feel it six months in, usually when you try to ship to Firefox or upgrade a dependency.

This post breaks down all three by the things that actually matter day to day, then gives you a straight decision guide. No “it depends” cop-outs.

TL;DR comparison table

Dimension
WXT
Plasmo
CRXJS

What it is
Full framework
Full framework
Vite plugin

Bundler
Vite
Parcel (custom)
Vite

Frontend frameworks
React, Vue, Svelte, Solid, vanilla
React-first
Any (you wire it)

Cross-browser
Chrome, Firefox, Safari, Edge
Chrome, Firefox, Edge
Chrome / Edge (Chromium)

MV2 + MV3
Both, simultaneously
Both, per build
Pick one

File-based entrypoints
Yes
Yes
No (manual manifest)

Auto-imports
Yes
No
No

Built-in storage / messaging
Yes (+ i18n)
Yes
No

Content-script HMR
Good (UI)
React only
Best (state-preserving)

Publishing / zip
Zip + Firefox source + publish
Zip + publish + Itero TestBed
None

GitHub stars (mid-2026)
~9.5k
~12k
~3.5k

Maintenance
Active
Maintenance mode
Revived (v2.0, Jun 2025)

Best for
Most new projects
React teams, more tutorials
Experts who want control

Sources for these claims are linked at the bottom. Star counts and bundle numbers move, so treat them as a snapshot, not gospel.

The 30-second answer

Starting fresh and want the safe default? Use WXT.

All-in on React and want the biggest pile of existing tutorials? Plasmo still delivers, with caveats.

You are a power user who wants minimal magic and full control of your Vite config? CRXJS.

Now the why.

WXT: the modern default

WXT brings Nuxt-style conventions to extensions. You drop files into an entrypoints/ directory and WXT generates the manifest, registers content scripts, and handles routing for you.

// entrypoints/content.ts
export default defineContentScript({
matches: (‘*://*.github.com/*’),
main() {
console.log(‘Hello from a WXT content script’);
},
});

Enter fullscreen mode

Exit fullscreen mode

Storage is a type-safe, reactive wrapper over browser.storage:

import { storage } from ‘#imports’;

const theme = storage.defineItem’light’ | ‘dark’>(‘local:theme’, {
fallback: ‘dark’,
});

await theme.setValue(‘light’);

Enter fullscreen mode

Exit fullscreen mode

Why people pick it:

Vite under the hood. Near-instant dev server start, fast builds.

Framework-agnostic. React, Vue, Svelte, Solid, or vanilla, all first-class. Want to use Svelte to shave bundle size? Go ahead.

True cross-browser. One codebase builds for Chrome, Firefox, Safari, and Edge, and it papers over the chrome.* vs browser.* namespace mess and MV2/MV3 differences.

Best-in-class dev mode. It opens a browser with the extension already installed and gives you HMR for UI.

Publishing built in. It can produce per-browser zips and even the Firefox source archive reviewers ask for.

Healthy project. Around 9.5k stars and active maintenance as of mid-2026, with production users like Eye Dropper (1M+ users) and ChatGPT Writer (600k+).

Watch-outs: it is the newest of the three, so there are fewer Stack Overflow answers than Plasmo. The conventions are opinionated, which is the point, but you do have to learn them.

Choose WXT if you are starting a new project and want speed, cross-browser support, and a maintained foundation without locking yourself into one UI library.

Plasmo: the React-first framework with the most tutorials

Plasmo was the framework that made extension dev feel like Next.js. Create a popup.tsx, export a React component, done. It still has the largest content library and the slickest first-run experience for React developers.

// content.tsx
import type { PlasmoCSConfig } from “plasmo”

export const config: PlasmoCSConfig = {
matches: (“https://www.github.com/*”)
}

const Overlay = () => div>Injected by Plasmodiv>
export default Overlay

Enter fullscreen mode

Exit fullscreen mode

Storage and messaging ship as dedicated packages:

import { Storage } from “@plasmohq/storage”

const storage = new Storage()
await storage.set(“theme”, “dark”)

Enter fullscreen mode

Exit fullscreen mode

Strengths:

React DX is excellent. Content Script UI with automatic Shadow DOM isolation is genuinely nice.

Biggest learning resource pool. ~12k stars and years of blog posts, videos, and starter repos.

Itero TestBed. A commercial product from the same team for staging and beta distribution before you hit the store, which none of the others offer.

The elephant in the room: maintenance. Multiple sources (including WXT’s own comparison) describe Plasmo as “in maintenance mode with little to no maintainers.” It is still on an older major version of Parcel, and that lag has real consequences, for example it blocks TailwindCSS v4. The high star count is partly legacy momentum, not current velocity. The framework is also still labeled alpha.

Choose Plasmo if you are React-only, you value the depth of existing tutorials and the Itero workflow, and you can accept the risk that feature development has slowed.

CRXJS: not a framework, a superpower for your Vite config

CRXJS (@crxjs/vite-plugin) is the odd one out: it is a Vite plugin, not a framework. You keep full ownership of your vite.config.ts and your project structure. CRXJS just teaches Vite how to build an extension, generate the manifest, and do hot reload.

// vite.config.ts
import { defineConfig } from ‘vite’
import { crx } from ‘@crxjs/vite-plugin’
import manifest from ‘./manifest.json’

export default defineConfig({
plugins: (crx({ manifest })),
})

Enter fullscreen mode

Exit fullscreen mode

Its killer feature is content-script HMR that preserves page state. Edit a content script, see it update on the page without a full reload or losing your place. For UI-heavy content scripts, that feedback loop is the fastest of the three.

The trade-off is that you get no abstractions. No built-in storage wrapper, no messaging helper, no i18n. You bring your own libraries (or raw chrome.* APIs) for everything. Cross-browser is effectively Chromium-only (Chrome and Edge); Firefox support is not the strength here.

CRXJS also has history: it sat in beta for over three years before v2.0 shipped in June 2025. It was recently revived with new maintainers, which is encouraging, but it is the smallest project of the three (~3.5k stars).

Choose CRXJS if you are an experienced developer who wants minimal magic, total control over the build, and the best content-script HMR, and you are happy to assemble the rest of the stack yourself.

Head-to-head on what matters

Build speed and bundle size

WXT and CRXJS both ride Vite (native ESM in dev, esbuild pre-bundling), so dev server start is near-instant. Plasmo’s custom Parcel bundler is the slowest, and developers migrating off it consistently report faster builds afterward. On output size, one benchmark put a WXT build around 400 KB versus roughly 800 KB for the Plasmo equivalent, though a lot of that comes down to your UI library choice (Svelte vs React) rather than the framework alone.

Cross-browser

This is where WXT pulls clearly ahead: one codebase to Chrome, Firefox, Safari, and Edge, with MV2/MV3 handled for you. Plasmo covers Chrome, Firefox, and Edge. CRXJS is realistically Chromium-only. If Firefox or Safari is on your roadmap, that decision is basically made.

Built-in APIs

WXT and Plasmo both give you storage, messaging, and content-script UI out of the box; WXT adds i18n. CRXJS gives you nothing here by design. Less code to maintain vs more control, pick your philosophy.

Maintenance health

For a project you will maintain for years, this is arguably the most important column in the table. WXT is actively developed. CRXJS is freshly revived. Plasmo’s slowdown is the real risk, and a stalled bundler dependency tends to quietly block the modern tools you will want later.

A simple decision tree

Need Firefox or Safari? WXT.

React-only team that lives in tutorials and wants Itero TestBed, and you accept maintenance risk? Plasmo.

Senior dev who wants a bare Vite setup and the fastest content-script HMR? CRXJS.

Anything else, or you just want the boring safe choice? WXT.

Building is only half the job

Here is the part nobody puts in the framework README: the framework decides how fast you build. It does nothing for whether anyone finds and installs your extension.

The Chrome Web Store is a discovery black box. New extensions launch invisible, ranking is heavily influenced by early reviews and keyword placement in your title and description, and you get almost no analytics on where your install funnel leaks (impressions to listing to install to retention).

A few practical things that move the needle once your code is shipped, regardless of which framework you chose:

Get your first real reviews early. Ranking and social proof both stall without them, and fake reviews get your listing pulled.

Make the listing assets convert. Icon, screenshots, and the small promo tile do more for install rate than another feature.

Watch your category. Knowing what competing extensions rank for and what users complain about is cheaper than guessing.

This is the gap ExtensionBooster is built for. It is a growth platform for extension and app developers: a credit-based system for getting real, store-compliant reviews and installs (with rating protection so low-star reviews do not cost you credits), market and competitor analytics to track ratings/users in your category and mine common complaints, and showcase pages that double as SEO backlinks. It also ships free developer tools that are genuinely useful no matter your stack, an icon generator, a screenshot maker, and a promo-tile cropper for the Web Store listing.

Worth bookmarking the free tools before your first submission, since you will need those listing assets the day you ship.

FAQ

Is WXT better than Plasmo?For most new projects in 2026, yes, mainly because it is actively maintained, Vite-fast, and works across all browsers. Plasmo still wins on React-specific tutorial depth and its Itero TestBed.

Is CRXJS a framework?No. It is a Vite plugin that adds Chrome extension support to a Vite project. You keep full control of the config and provide your own storage/messaging/i18n.

Can I migrate from Plasmo to WXT?Yes, and it is a common move. Expect to rewrite entrypoint conventions and swap @plasmohq/storage for WXT’s storage API, but builds and dev startup get noticeably faster.

Which is best for a React extension?Both WXT and Plasmo handle React well. Plasmo is React-first with more examples; WXT gives you React plus a maintained, cross-browser foundation and the option to use a lighter UI library later.

Which has the best hot reload?CRXJS for content scripts (state-preserving). WXT for overall UI dev experience. Plasmo’s HMR is tuned for React and falls back to full reloads otherwise.

Bottom line

WXT is the default I would hand a new team in 2026.

Plasmo is still a strong React experience if you accept the maintenance risk and want the Itero workflow.

CRXJS is the connoisseur’s choice for control and content-script HMR.

Pick the build tool that fits your team, then put real energy into the part that frameworks ignore: getting discovered and installed.

Sources: WXT official comparison, WXT homepage, Plasmo docs, The 2025 State of Browser Extension Frameworks, and project GitHub repos.



Source link