I've been running Trustfolio for a few months now, and the one decision that keeps coming up in sales calls is the widget bundle size.
People don't buy a testimonial tool because it's 4.8KB. But they stop looking at alternatives the moment they realize the competitor's widget drops their Lighthouse score by 15 points.
So here are the exact choices that kept our embed under 5KB while the category average sits at 65–125KB gzip.
The benchmark (measured with Chrome DevTools, 2026)
| Widget | Layout | Transfer size (gzip) |
|---|---|---|
| Testimonial.to | Wall | 125 KB |
| Senja | Carousel | 65 KB |
| Famewall | Wall | 88 KB |
| Trustfolio | Wall | 4.8 KB |
| Trustfolio | Carousel | 3.9 KB |
| Trustfolio | Badge | 2.1 KB |
These numbers came from Chrome DevTools' Network tab with the "Disable cache" + "Slow 3G" options. I'm comparing equivalent rendered outputs, not the vendor's marketing numbers.
Choice #1 — Ship vanilla JS. No framework runtime.
Every competitor I audited ships some flavor of React or a micro-framework at runtime. React plus ReactDOM alone is ~45KB gzip — that's already 9x our entire widget.
Our embed is a single IIFE. It reads the data-trustfolio attribute, fetches JSON from an edge route, and renders DOM directly. No JSX, no virtual DOM, no reconciler.
const init = async () => {
for (const mount of document.querySelectorAll('[data-trustfolio]')) {
const id = mount.dataset.trustfolio;
const data = await fetch(`https://trustfolio.dev/api/widgets/${id}/data`).then(r => r.json());
const shadow = mount.attachShadow({ mode: 'closed' });
render(shadow, data);
}
};
init();
render() is a small set of layout functions (wall, carousel, badge, popup, marquee, card) that directly assemble DOM nodes. Each layout lazy-loads its handlers on first interaction — carousel's swipe handler is only fetched when the user actually swipes.
Choice #2 — Shadow DOM, not CSS-in-JS.
Testimonial widgets have to deal with hostile host styles. Your widget might be rendered inside a WordPress theme that has * { box-sizing: border-box } on every element, or a Tailwind Preflight that strips all default margins.
Most vendors solve this by shipping their styles inline or via CSS-in-JS — which is why you see styled-components or emotion in their bundles.
We use a closed Shadow DOM. The shadow root has its own style scope. No host style can leak in. No widget style can leak out. And we don't ship a runtime because the browser handles encapsulation natively.
Stylesheet is a <style> element inside the shadow, declarative, zero runtime.
Choice #3 — async script + navigator.sendBeacon.
The script is async, so it never blocks rendering. Impression and click analytics use navigator.sendBeacon, which queues the request in the browser's network stack and returns instantly — no awaiting a fetch promise, no perf regression on user interaction.
function track(event, data) {
const blob = new Blob([JSON.stringify({ event, ...data })], { type: 'application/json' });
navigator.sendBeacon('/api/analytics', blob);
}
The analytics endpoint writes to an edge-adjacent table. No third-party tracking scripts. No cookies. The widget is GDPR-trivial because it collects nothing a user wouldn't expect.
Choice #4 — No web fonts.
We render testimonials with font-family: system-ui, -apple-system, sans-serif. Native UI font on every platform. Zero KB of font download. Zero FOIT/FOUT.
Competitors that load Google Fonts pay 20–80KB per weight for the privilege of a slightly prettier wall of quotes. Not worth it.
Choice #5 — Bundle once, not per layout.
All six layouts share the same bundle. Dead-code elimination via esbuild removes unused layouts at build time — but the runtime itself is one file. The tradeoff: the carousel user downloads ~0.7KB of wall-specific code they'll never run. The win: one HTTP request instead of six, one cached URL instead of a cache-busting nightmare per layout combination.
What this unlocks for customers
- Shopify product pages that still score >95 on Lighthouse with social proof embedded.
- Framer landing pages that keep their signature "single HTTP request" speed.
- WordPress sites that don't need to install a plugin (just a
<script>tag).
None of this is magical. It's just a refusal to ship more than the feature requires. Every line of code I considered adding, I asked: "does this change what the user actually sees?" If not, it didn't go in.
The wider point
A lot of modern SaaS ships weight by default because frameworks make it easy and bundlers don't warn you until it's already too late.
Testimonial widgets are a small niche, but the same pattern applies to every embedded SaaS asset — chat bubbles, cookie banners, analytics scripts, A/B testing agents. They accumulate until your site is 60% third-party JS.
If you're building one, the question worth asking is: what's the smallest thing that still delivers the feature? Not "what's the easiest stack to reach for?"
Trustfolio collects customer testimonials, scores them with AI for conversion potential, and embeds them on any site with a single <script> tag. Free tier, 15 testimonials, no credit card.
PS: for the first founder reading this, FOUNDER100 gives you 100% off the first month of Pro or Business. 24 hours, one use.
Top comments (0)