Sep 1, 2026

Building Interactive Cards from Design JSON Without Killing Your Feed: Overlays, Video, Mute/Unmute, and Lag-Free Lists

Learn how to turn design JSON into interactive, video-enabled cards using overlays, smart media controls, caching, and virtualization without slowing down high-cardinality feeds.

Author

Deepanshu GoyalSenior Software Engineer - III
Building Interactive Cards from Design JSON Without Killing Your Feed: Overlays, Video, Mute/Unmute, and Lag-Free Lists

Why Design JSON Gets Complicated in Production

You already have a well-defined JSON document for a designed card.

It might come from Canva, a Canva-like editor, or any canvas library that exports nodes with positions, sizes, images, text, videos, and links. The JSON is not the hard part. The hard part is what product asks for next:

Turn that JSON into a live UI card where regions are clickable / redirectable, videos play in the right place, the card can animate (flip, scale, snap), a list of many cards still feels smooth, and media behavior is correct, mute by default, unmute after gesture, play on hover or only the active card plays.

A demo with one card is easy. Production is a feed:

  • Dozens of cards may be reachable in one session
  • Every card wants fidelity
  • Every video wants a decoder
  • Every click competes with card animation and scroll

If you “just render the whole JSON tree” with a heavy design runtime (full canvas/SVG scene graph per card), the first card looks perfect and the tenth card makes the list lag. If you flatten everything to a static image, the list is smooth and interactivity dies.

This article is about the custom architecture we used to keep both: authored fidelity in a high-cardinality list, without dropping an editor engine into every cell.

The goal is not “draw JSON to the screen.” It is: build interactive, animated, video-capable cards from design JSON, with custom code, without letting the feed collapse.

Why a Dedicated Architecture is Necessary

Design JSON looks renderable until production constraints show up:

  1. List cost is multiplicative: Rehydrating every shape/text/image node per card is fine for an editor, expensive for a carousel of twenty cards.
  2. Clicks fight animations: If the card flips on press, and a CTA also lives on the card, event ownership must be explicit.
  3. Video is a budget: Autoplay policies, audio focus, and decoder limits mean “play everything visible” is not a strategy.
  4. Most of the design is static: Backgrounds, typography, and photos usually do not change after export. Rebuilding them at list-bind time is wasted work.
  5. Mute/unmute is product behavior: Browsers block unmuted autoplay; users still expect sound after the first gesture, without audio leaking from off-screen cards.
  6. Cache the wrong thing and you thrash: Either you re-download and remount constantly, or you keep too many live players alive.

Those constraints are why we stopped treating “render the design JSON” as one function, and split the problem into bake, extract, composite, schedule, and virtualize.

What we Tried First (and What Broke)

We did not start with a hybrid compositor. We shipped the obvious approach first: rehydrate the full design JSON into a live scene graph per card (canvas/SVG-style runtime) inside the list.

Field observations

Experiment attempt

What we expected

What actually happened

Full JSON → scene graph per card

Pixel-perfect fidelity in the feed

First 1–2 cards fine; under our Chrome profile (~40 nodes/card, 30-card list), scroll accumulated multi-hundred-ms scripting by the time ~8–12 full runtimes were mounted (see benchmark)

Let every mounted card autoplay video

“Alive” feed

Decoder contention + audio leaks from off-center cards

Rely on the design runtime for clicks

CTAs “just work”

Card flip/animation handlers raced link taps; suppress was unreliable

Remount cards as the window moved

Lower memory

Remount flicker + repeated image/video setup while scrubbing nearby

One cache for “everything about the card”

Simpler code

Playing state leaked across cards; reopen/remount felt broken

The cut we made after those failures

Cut from the feed

Kept

Live rehydration of static shapes/text/images

Design JSON as source of truth for behavior

Design-engine runtime inside list cells

Baked static image as the visual base

Play-on-mount

Explicit play schedule (hover or active-card)

Implicit hit handling inside the design engine

Custom overlay hit boxes + suppress-flip flag

One mega-cache

Separate caches for images, JSON, overlays, players, gesture

Practitioner takeaway: if most pixels are fixed at export time, rehydrating them on every list bind is not fidelity, it is wasted work. The hybrid model is what remained after measuring that.

The Architecture: Bake, Extract, Composite, and Schedule

Design JSON workflow for interactive cards with video layers, clickable regions, and media controls.
High-level: JSON to interactive cards

Think in layers:

Layer

What it is

Why

Static base

A baked image of the designed card (or a cheap image export)

Carries 90%+ of visual fidelity for free

Design JSON

Source of truth for bounds + behavior

Only used for what must run at runtime

Overlay runtime

Videos + clickable hit regions

Custom DOM/native views, not a full design engine

List policy

Mount window + play policy

Keeps scroll near interactive frame rates

Media policy

Mute-first; gesture = unmute hint; prefer explicit unmute; hover or active-card play

Matches browser autoplay reality

JSON can come from anywhere that can describe a tree of nodes with transforms and assets: Canva export, an internal canvas editor, Figma plugin output, etc. The client architecture does not care which tool authored it, as long as the schema is stable.

1. Where the JSON comes from (and what you should export)

Workflow from canvas design to structured JSON, optional bitmap preview, and API storage.
Where the JSON comes from

A useful export usually includes:

  • canvas / page width, height
  • a tree of items: rect, text, image, video, group, …
  • per-item transform (x, y, rotation/scale if needed)
  • asset URLs for images/videos
  • link fields on buttons, text, or groups

Optionally, and this is the production unlock, also export a static preview bitmap of the finished design.

// Illustrative schema (yours will differ)
type DesignDocument = {
  width: number;
  height: number;
  items: DesignItem[];
  previewImageUrl?: string; // baked PNG/JPEG of the card face
  hasInteractiveContent?: boolean; // videos or links present
};
 
type DesignItem =
  | {
      type: 'video';
      x: number;
      y: number;
      width: number;
      height: number;
      src: string;
      loop?: boolean;
    }
  | {
      type: 'text';
      x: number;
      y: number;
      width: number;
      text: string;
      url?: string;
    }
  | {
      type: 'group';
      x: number;
      y: number;
      link?: string;
      children: DesignItem[];
    }
  | { type: 'image' | 'rect' | string /* … */ };

Why bake an image at all? Because most nodes only contribute static visuals. If you already have a correct raster of the design, list rendering becomes “show image + attach behavior,” not “re-layout a design tool in every cell.”

If your exporter cannot bake an image yet, you can still rasterize once on the server or in a headless render step after publishing. The important idea is: do static work once, outside the scroll path.

2. Extract only what must run at runtime

Runtime extraction workflow for video, links, buttons, and overlays from a design JSON tree.
Extract only what must run at runtime

Browse-time code should not recreate the whole design. Walk the JSON and emit a flat list of interactive overlays:

  • videos → media overlays
  • nodes with URLs → link hit boxes
  • groups with links → one hit box over the group bounds

Ignore static shapes/text/images if they are already in the bitmap.

What This Overlay Mapping Supports and What It Doesn’t

The examples below assume axis-aligned translation (and optional uniform parent scale) for overlay placement: nested group offsets accumulate as ox/oy, then map into the card with the contain-fit math in §3.

Scoped out of this article’s production recipe (unless you extend the mapper):

  • non-zero rotation on videos/links/groups
  • non-uniform scaleX/scaleY that changes hit geometry
  • skew / arbitrary matrices
  • transformed groups whose children need oriented (rotated) hit regions

If your Canva-like export commonly emits rotated interactive nodes, either bake those interactions into the bitmap as non-interactive paint, or compute a full affine transform and oriented bounding box, do not silently use left/top/width/height axis-aligned boxes.

type Overlay =
  | {
      kind: "video";
      x: number; y: number; width: number; height: number;
      src: string; loop?: boolean; muted?: boolean;
    }
  | {
      kind: "link";
      x: number; y: number; width: number; height: number;
      url: string;
    };
 
function extractOverlays(items: DesignItem[], ox = 0, oy = 0): Overlay[] {
  const out: Overlay[] = [];
  for (const item of items) {
    if (item.type === "group") {
      const x = ox + item.x;
      const y = oy + item.y;
      out.push(...extractOverlays(item.children, x, y));
      if (item.link) {
        // compute bounds from children or explicit group size
        out.push({ kind: "link", x, y, width: /*…*/, height: /*…*/, url: item.link });
      }
      continue;
    }
    if (item.type === "video") {
      out.push({
        kind: "video",
        x: ox + item.x,
        y: oy + item.y,
        width: item.width,
        height: item.height,
        src: item.src,
        loop: item.loop,
      });
    }
    if ("url" in item && item.url) {
      out.push({
        kind: "link",
        x: ox + item.x,
        y: oy + item.y,
        width: item.width,
        height: /* text/image height */,
        url: item.url,
      });
    }
  }
  return out;
}

Why this exists in production: extraction cost scales with interactive nodes, not with every decorative rectangle in the design.

3. Hybrid compositor: image + video + click targets

Hybrid compositor stack aligning bitmap, video, and link hit boxes with contain-fit mapping.
Hybrid compositor stack

Contain-fit mapping (scale and letterbox offsets)

object-fit: contain (or equivalent) scales the bitmap uniformly and centers it. Using only Math.min(cw/docW, ch/docH) for overlay left/top is unsafe when the container aspect ratio differs from the document: the image is letterboxed, so overlays must apply the same X/Y offsets as the painted bitmap.

type Fit = {
  scale: number;
  offsetX: number; // letterbox / pillarbox
  offsetY: number;
  contentW: number;
  contentH: number;
};
 
/** Match CSS object-fit: contain for a document inside a card box. */
function containFit(docW: number, docH: number, boxW: number, boxH: number): Fit {
  const scale = Math.min(boxW / docW, boxH / docH);
  const contentW = docW * scale;
  const contentH = docH * scale;
  return {
    scale,
    offsetX: (boxW - contentW) / 2,
    offsetY: (boxH - contentH) / 2,
    contentW,
    contentH,
  };
}
 
function styleFor(o: Overlay, fit: Fit) {
  return {
    position: "absolute" as const,
    left: fit.offsetX + o.x * fit.scale,
    top: fit.offsetY + o.y * fit.scale,
    width: o.width * fit.scale,
    height: o.height * fit.scale,
  };
}

Prefer sharing one layout root so image and overlays cannot drift:

function InteractiveCard({ doc, width, height }: Props) {
  const overlays = useMemo(() => extractOverlays(doc.items), [doc]);
  const fit = containFit(doc.width, doc.height, width, height);
 
  return (
    <div style={{ position: "relative", width, height, overflow: "hidden" }}>
      {/* Position the bitmap with the SAME fit math as overlays (avoid relying on
          object-fit alone unless you also read the letterboxed content box). */}
      <img
        src={doc.previewImageUrl}
        alt=""
        style={{
          position: "absolute",
          left: fit.offsetX,
          top: fit.offsetY,
          width: fit.contentW,
          height: fit.contentH,
        }}
      />
 
      {overlays.filter((o) => o.kind === "video").map((v, i) => (
        <video
          key={i}
          src={v.src}
          loop={v.loop ?? true}
          playsInline
          muted
          style={styleFor(v, fit)}
          ref={(el) => registerVideo(el)}
        />
      ))}
 
      {overlays.filter((o) => o.kind === "link").map((l, i) => (
        <a
          key={i}
          href={l.url}
          target="_blank"
          rel="noreferrer"
          style={{ ...styleFor(l, fit), zIndex: 2 }}
          onClick={(e) => {
            e.stopPropagation();
            onLinkClick?.();
          }}
        />
      ))}
    </div>
  );
}

Production note: if you must use object-fit: contain on a full-bleed <img width={boxW} height={boxH}>, compute offsets from the same contain formula, or measure the content box, and feed those offsets into styleFor. Never mix “contain image” with “scale-only overlays.”

No design-library runtime required in the list. You are compositing HTML (or native views) on purpose.

On mobile, the same idea applies: one cached image, native video views, and either overlay views or a coordinate hit-test map on tap (often cheaper inside recycled cells), with the same contain-fit offsets.

4. Video: placement, mute/unmute, hover vs active-card play

Video scheduling workflow with mute-first playback, explicit unmute, and pause when inactive.
Video playback scheduling

Placement

Video is a spatial problem: same rectangle as in the design JSON, mapped through containFit. You do not need timeline sync with the rest of the card. The bitmap stays still; the clip loops in its hole.

Mute / unmute (browser reality qualified)

Mute-first playback is sound guidance. A stored “user has gestured” flag is not a universal license for future audible autoplay:

  • Browser autoplay policies differ (Chrome, Safari, iOS WKWebView, Firefox) and can still reject audible play() even after a prior gesture, especially across navigations, new media elements, or when the gesture is not considered tied to that media.
  • Treat localStorage / session “gestured” as a hint to attempt unmute, not as permission.
  • Always keep the play() failure path (catch → force muted → retry muted).
  • Prefer an explicit user-controlled unmute control on the active card (tap speaker / “tap for sound”) when audible playback matters for UX trust.

const activeVideos = new Set<HTMLVideoElement>();
 
async function smartPlay(video: HTMLVideoElement, preferUnmuted: boolean) {
  activeVideos.add(video);
  video.muted = !(preferUnmuted && userHasGesturedHint());
 
  try {
    await video.play();
  } catch {
    // Autoplay / audible policy may still reject , fail closed to muted.
    video.muted = true;
    try {
      await video.play();
    } catch {
      // Leave paused; UI can show a play/unmute affordance.
    }
  }
}
 
function tryUnmuteActiveAfterGesture() {
  markGesturedHint();
  activeVideos.forEach((v) => {
    if (v.paused) return;
    v.muted = false;
    v.play().catch(() => {
      v.muted = true; // policy rejected audible playback
    });
  });
}
 
// Preferred when sound is important:
function onUnmuteButtonClick(video: HTMLVideoElement) {
  markGesturedHint();
  video.muted = false;
  video.play().catch(() => {
    video.muted = true;
  });
}

Never unmute inactive/off-screen cards (audio leaks feel like bugs), regardless of gesture state.

Play policy: pick one (or both by surface)

Policy

When to use

Rule

Active-card play

Carousels / snap lists

Only 'index === activeIndex' calls 'play()'; others 'pause()' / unload

Hover-to-play

Desktop grids

'mouseenter' → play, 'mouseleave' → pause

Visible face only

Flipping cards

Play the face the user sees; pause the hidden face

// Active-card example
useEffect(() => {
  if (isActive) playAll(cardVideos);
  else pauseAll(cardVideos);
}, [isActive]);
 
// Hover example
<div onMouseEnter={() => playAll(cardVideos)} onMouseLeave={() => pauseAll(cardVideos)}>
  <InteractiveCard … />
</div>

Production rule: mounting a card must not mean decoding. Play is a schedule, not a side effect of render.

5. Card animation without breaking clicks

Gesture workflow prioritizing link redirects over card flip animations and scrolling.
Gesture ownership: click vs card animation

Cards often flip, scale, or snap. That animation layer will steal taps if you are not careful.
Custom pattern that works:

  1. Card container listens for “flip/open” gestures
  2. Link overlays call stopPropagation() and set a suppress-next-flip flag
  3. Card handler checks the flag before animating
const suppressFlip = useRef(false);
 
function onCardPointerUp() {
  if (suppressFlip.current) {
    suppressFlip.current = false;
    return;
  }
  setFlipped((v) => !v);
}
 
// on link overlay:
onClick={() => {
  suppressFlip.current = true;
  // navigate / window
}};

If a face is hidden mid-flip, render it as image-only (no videos/overlays) so you do not pay for two live interactive faces at once.

6. No lag: list virtualization, mount windows, and caching

Bounded window workflow for smooth card lists using live mounts, LRU caching, and video gating.
Keeping the list smooth

Smoothness is mostly about what you refuse to do for far cards.

Keep Only a Bounded Set of Cards Mounted

An expand-only range (once mounted, never unmount) reduces remount flicker while scrubbing nearby, but in a long session it can eventually retain most of the list and defeat virtualization. Prefer a bounded retained window with LRU-style eviction:

  1. Always keep a hard live window around the active index (active ± N).
  2. Optionally retain a small extra set of recently visited indices (LRU, capped at K) to smooth short back-and-forth scrubbing.
  3. Evict anything outside live ∪ retainedLRU back to placeholders / recycled shells.
  4. On eviction: pause media, detach players, keep image/JSON caches warm so remount is cheap.
const LIVE_RADIUS = 2;      // always mounted around active
const LRU_CAP = 6;          // recently visited extras (hard cap)
const retained = useRef<number[]>([]); // LRU order, newest at end
 
function indicesToMount(active: number, total: number): Set<number> {
  const live = new Set<number>();
  for (let i = active - LIVE_RADIUS; i <= active + LIVE_RADIUS; i++) {
    if (i >= 0 && i < total) live.add(i);
  }
 
  // touch active in LRU
  retained.current = retained.current.filter((i) => i !== active);
  retained.current.push(active);
  while (retained.current.length > LRU_CAP) retained.current.shift();
 
  const mount = new Set(live);
  for (const i of retained.current) {
    if (mount.size >= LIVE_RADIUS * 2 + 1 + LRU_CAP) break;
    if (i >= 0 && i < total) mount.add(i);
  }
  return mount;
}
 
// render:
 // i ∈ mount → InteractiveCard (image + overlays; video only if active/hover policy says so)
 // else → empty absolute placeholder (preserves scroll width)

When a short expand-only burst is still safe: e.g. during a single gesture/animation frame burst, or for a tiny catalogue (total ≤ LIVE + LRU). Do not present unbounded expand-only as the default production policy for unbounded feeds.

Clamp snap to one card at a time on aggressive flicks so index jumps do not skip content or start multiple videos.

Recycled cells (mobile-shaped)

Use a recycler (FlashList, RecyclerListView, etc.):

  • Recycle card shells
  • Run scroll-driven scale animations on the UI thread when possible
  • Gate native video mounts to the active index
  • Defer player creation until scroll settles
  • Show a poster/thumbnail until first frame
  • Unload players on recycle

What to cache (intentionally)

Cache

Stores

Horizon

Problem it solves

Image / CDN cache

Baked card bitmaps

Browser/CDN/disk

Avoid re-fetch/re-decode of the same face when still cached

JSON / query cache

Design documents for visible + nearby cards

Session / React Query-like

Avoid refetch while scrubbing the list

Overlay extract memo

'extractOverlays(doc)' result

While doc identity is stable

Avoid re-walking JSON every render

Video element lifecycle

Players for active card only

While active

Avoid decoder storms

Gesture hint

“user has interacted” (optional)

Session

Prefer unmuted attempt never treat as hard permission

Do not cache “playing state” globally across all cards. Cache assets and extracted geometry; schedule playback separately. Keep image cache warm across bounded remounts so eviction stays cheap.

Mobile list workflow using recycled cells, gated video mounts, posters, and player unloading.
Mobile FlashList + gated video mounts

How the Hybrid Approach Performs Against a Full Design Runtime

Behavior

Full design runtime per card

Hybrid custom compositor

Static visuals in a list

Rebuild node trees

One baked bitmap + overlays

Interactivity

Entire graph live

Videos + hit boxes only

Click vs flip

Easy to get wrong

Explicit suppress path

Videos in a carousel

Many decoders

Hard cap: active/hovered card

Far cards

Expensive

Placeholders / recycled shells

Cost wording (precise)

Bitmap compositing removes dependence on the number of design nodes for paint. That is not the same as “O(1) image decode.” Decode/upload cost still depends on:

  • Pixel dimensions and encoding (JPEG/WebP/PNG)
  • Whether the image is already in memory/disk HTTP cache
  • Platform decode path (browser vs native)

Say instead: work no longer scales with design-node count; cold decode remains image-size/cache dependent.

Reproducible micro-benchmark (example)

We re-ran a focused comparison after correcting the architecture claims. Treat this as a reproducible recipe, not a universal SLA, re-measure on your devices.

Item

Value

Device

M2 MacBook Air, Chrome 131 (macOS)

Card box

400×580 CSS px

Document

1080×1920 design JSON, ~40 nodes, 1 looping muted MP4 (≤720p, ~3s)

Baked face

WebP ~180–220 KB (approx), warm CDN after first fetch

List

horizontal snap carousel, 30 cards, same document cloned

Metrics

Chrome Performance panel + 'performance.measure'; long-task count during a 3s programmatic scroll

Policy

hybrid: bounded mount ('LIVE_RADIUS=2', 'LRU_CAP=6'), active-card-only video; baseline: full scene-graph runtime per mounted card, videos eligible when mounted

Observed on that setup (warm image cache, median of 5 runs):

Signal

Full JSON runtime in feed

Hybrid compositor

Scripting + layout during 3s scroll (approx)

~180–240 ms accumulated

~45–70 ms accumulated

Long tasks (>50 ms) during scroll

4–7

0–1

Concurrent '<video>' elements intending play

up to ~5–9 near center

1 (active)

Overlay drift with contain letterboxing

N/A (scene graph)

0 px when using 'containFit' offsets; noticeable drift if scale-only

How to reproduce:

  1. Mount 30 identical cards in a snap carousel.
  2. Cold vs warm: run once to fill HTTP cache, then measure.
  3. Programmatically scroll across ~10 indices in 3 seconds.
  4. Compare full scene-graph cells vs hybrid cells with the same assets.
  5. Record Performance panel scripting time, long tasks, and document.querySelectorAll('video').length for non-paused players.

What Changes After Moving to the Hybrid Architecture

Signal

Before (full JSON runtime in feed)

After (hybrid custom compositor)

Paint cost vs design-node count

Scales with nodes

Independent of node count (bitmap + N overlays)

Active decoders in a carousel

O(visible interactive cards)

Hard cap ≤ 1 (active-card) or 1 hovered

Link tap vs card flip

Race / double-fire

Overlay owns gesture; flip suppressed

Long browse sessions

Remount thrash or unbounded retain

Bounded live window + capped LRU retain

Audible autoplay

Assumed after gesture

Gesture = hint; 'play()' may still fail; prefer explicit unmute

Contain letterboxing

Often ignored

'scale + offsetX/offsetY' shared with bitmap

Operable SLOs

  1. Active playing videos in the carousel = 0 or 1
  2. Link taps must not trigger card flip (mis-tap rate on overlay hits → 0)
  3. During warm-cache scroll with no new video start, aim for few/no >50 ms long tasks on your target mid-tier device (re-measure; do not cargo-cult “16 ms” without a profile)

If you cannot assert those in instrumentation, you do not have the architecture yet, you have a demo.

Architecture Alternatives: When to Choose What

1. Hybrid bitmap + extracted overlays (what we recommend for feeds)

Prefer when designs are authored once, most pixels are static, and cards live in a list.

Cost: bitmap and JSON must stay in sync; changing static art means rebaking.

2. Full canvas/SVG runtime per card

Prefer for editors and single-document detail views.

Weak for carousels with many interactive cards.

3. Static images only

Prefer for lightweight catalogs.

Weak when product requires in-card video and redirects inside the feed.

Approach

Best for

Weak when

Hybrid custom compositor

Interactive cards in feeds

Constant redesign without rebake

Full design runtime per card

Editors / low cardinality

List performance + gesture fights

Static thumbnail only

Simple browsing

In-feed video/CTAs

Production Reliability Checklist

  • Measure the card container before mounting videos (avoid zero-size mounts)
  • Map overlays with contain-fit scale + letterbox offsets shared with the bitmap
  • Scope or explicitly handle rotation / non-uniform transforms, do not assume axis-aligned boxes
  • Start muted; treat gestured flags as hints; keep play() failure → muted retry; prefer explicit unmute UI when sound matters
  • Pause on tab hidden / app background; resume by policy on return
  • Image-only for hidden flip faces
  • Unload native players on recycle
  • Use a bounded live window + capped LRU retain, not unbounded expand-only
  • Keep video counts/resolution short enough that the active card peak stays bounded
  • Keep the design engine in authoring; keep the feed on the hybrid compositor

Where this Sits as Engineering Practice

This is custom product/platform work, not a library demo:

  • Product: cards must feel designed and alive in discovery, not only on a detail page
  • Platform: JSON contract, bake step, player lifecycle, gesture ownership, caches
  • Performance: mount windows and play policies are measurable trade-offs; re-run the micro-benchmark recipe on your target devices

If your JSON comes from Canva or any Canva-like tool, the opportunity is the same: treat export as a dual payload (appearance + behavior), then write a thin runtime for the feed.

Takeaways

  1. Design JSON can come from any Canva-like tool; the feed runtime should not embed that tool.
  2. Bake static appearance; extract only videos and click targets.
  3. Composite with contain-fit scale + offsets so media/hits stay aligned under letterboxing; scope rotation/affine cases explicitly.
  4. Mute-first; gesture flags are hints; preserve play() failure paths; prefer explicit unmute when sound matters.
  5. Choose an explicit play policy: hover or active-card (or both by surface).
  6. Make gesture ownership explicit so redirects win over card animation.
  7. Keep lists smooth with a bounded mount window + capped LRU, recycling, and caches for assets + extracted overlays, not “everything is playing,” and not unbounded expand-only.
  8. Replace vibes with at least one reproducible scroll/profile measurement on a named device/browser.

That is the difference between rendering a design document once and shipping interactive cards that survive a real feed.

Building Interactive Experiences That Hold Up in Production

The architecture here solves a specific card-rendering problem, but the principle applies much more broadly. Rich web experiences need visual fidelity, interaction behavior, media lifecycle, and performance to work as one system. The real test is not whether an interface works with one card or one user, but whether it stays responsive as content, traffic, and product complexity grow.

At GeekyAnts, our Web Engineering teams build and optimize performance-critical web experiences, from frontend architecture and rendering strategies to media-heavy interfaces and scalable product platforms.

Building an interaction-heavy web product or dealing with frontend performance bottlenecks? Talk to our Web Engineering team about an architecture designed to hold up in production.

Subscribe to Our Newsletter

RELATED ARTICLES

More from the engineering frontline.

Dive deep into our research and insights on design, development, and the impact of various trends to businesses.
The Bug That Doesn't Show Up in Code Review: Why Your Flutter Web App Reloads on Safari

Aug 19, 2026

The Bug That Doesn't Show Up in Code Review: Why Your Flutter Web App Reloads on Safari
A real-world look at how oversized images can trigger Safari reloads and iOS crashes in Flutter apps and how smarter image decoding prevents them.
From Prompting to Process: What Changed When Flutter Shipped Agent Skills

Aug 19, 2026

From Prompting to Process: What Changed When Flutter Shipped Agent Skills
This blog explores how Flutter Agent Skills improve AI-assisted development by combining official framework workflows with project-specific guidance for more consistent development.
Why Everything Your AI Builds Looks the Same

Aug 19, 2026

Why Everything Your AI Builds Looks the Same
This blog explores why AI-generated interfaces often look alike and explains how design systems, product context, and reusable engineering practices help teams build distinctive, scalable
How We Built the Missing Bridge from Code to Figma
Technology

Jul 10, 2026

How We Built the Missing Bridge from Code to Figma
This blog explores how AI-generated React apps get turned into fully editable, designer-ready Figma files by reading React Fiber instead of the DOM.
Building a Resilient Hybrid-Cloud Network with WireGuard HA, Route-Based Failover, and Deep Observability
Technology

Jun 27, 2026

Building a Resilient Hybrid-Cloud Network with WireGuard HA, Route-Based Failover, and Deep Observability
A practical breakdown of building resilient AWS-to-on-premises connectivity with WireGuard HA, active-standby failover, and deep packet-forwarding observability.
We Built a 114-Second AWS-to-Azure Failover. Here’s What We Learned
Technology

Jun 19, 2026

We Built a 114-Second AWS-to-Azure Failover. Here’s What We Learned
A practical guide to building a 114-second multi-cloud disaster recovery failover between AWS and Azure — what we built, what broke, and what we learned.
Cloud-Native and Cloud-Agnostic Are Not Ideologies; They Are Business-Stage Decisions
Technology

Jun 12, 2026

Cloud-Native and Cloud-Agnostic Are Not Ideologies; They Are Business-Stage Decisions
This blog explains how organizations can balance speed, scalability, and operational flexibility as they grow from startup to enterprise scale.

The Right Conversation Can Save You Six Months.

Whether you’re navigating AI adoption, modernizing legacy systems, or scaling a product - we start by listening. No pitch deck. No template. A real conversation.