Workflow8 min read

JSON-Driven After Effects: I Made It Work, and It's Still Painfully Slow

I build JSON-driven AE toolkits for platform demos. They work. They also grind the viewport to a halt, and nothing I've tried fixes it.

Published · 19 Aug 2026After Effects · Workflow · Automation · Expressions · JSONBy Ryan Johnson
markdown Markdown chevron down sm
download Download as Markdown

Why I wanted this in the first place

I make platform demo videos. That means product UI, on screen, with real-looking data in it, narrated by copy that legal will rewrite twice before it ships.

The math on that is ugly if you do it by hand. A full demo runs past a hundred expression-driven text layers once you count every screen and every precomp. Marketing changes "Threats detected" to "Threats identified." Someone notices the dashboard numbers don't match the numbers in the deck. A regional team wants it in German. Every one of those is a full pass through the project, layer by layer, and every pass is a chance to typo a number into a customer-facing video.

Data in, comps out. That's the whole pitch. One template, one JSON file, many outputs: versioned copy, corrected figures, localized strings, all of it living in a text file I can diff instead of a project file I can only eyeball.

I built it. It works. I got it working through sheer force of will, and I want to be honest about what that cost.

Where I'm coming from:

This is a working note from building JSON-driven toolkits for real deliverables, not a beginner tutorial. If you've never wired an expression to a footage item, start with Adobe's data-driven animation docs first.

How it's actually wired

The data file

Import a .json file into After Effects and it lands in the Project panel as a footage item. It doesn't show up as data streams in the timeline the way .mgjson does. It just sits there, and you reach into it from expressions.

Keep the schema boring. Flat-ish, addressable by ID, no cleverness:

demo-copy.json
{
"meta": { "version": "2026.08", "locale": "en-US" },
"scenes": [
  {
    "id": "detections",
    "headline": "Threats detected",
    "value": "1,284",
    "delta": "+12%",
    "rows": [
      { "label": "Prompt injection", "count": 412 },
      { "label": "Model extraction", "count": 98 }
    ]
  }
]
}

Reading it in an expression

footage("file.json").sourceData gives you the parsed object. From there it's plain JavaScript, applied straight to Source Text on an ordinary text layer in an ordinary comp:

headline-sourceText.js
// Source Text on the headline layer
const data = footage("demo-copy.json").sourceData;
const scene = data.scenes.find(s => s.id === "detections");

// Fail loud in the viewport, not silently at render
scene ? scene.headline : "MISSING: detections.headline";

Two things worth knowing here. find() and arrow functions need the modern JavaScript expression engine, which shipped in After Effects 16.0. On the Legacy ExtendScript engine this expression is a syntax error. And when you're indexing into text as an array, the engines disagree: the JavaScript engine wants text.sourceText.value[i] where Legacy took text.sourceText[i].

The failure string matters more than it looks. A missing key throws, the expression disables itself, and you find out three renders later. A string that says MISSING: in 60pt type in the middle of frame is a bug you catch immediately.

Fail loud:

Every data-bound text layer in my templates has a fallback string that names the exact key it couldn't find. It has saved me from shipping a blank lower-third more than once.

There's a second family of methods (dataValue(), dataKeyCount(), dataKeyTimes(), dataKeyValues()) that shipped alongside sourceData for pulling keyframed streams out of data files. Right tool if your data has a time dimension. Mine doesn't; my JSON is a copy deck, not a telemetry log, so sourceData is all I touch.

The other route, which I don't use

You can also expose properties in the Essential Graphics panel, export a MOGRT, and let someone fill it in downstream in Premiere. It's the sanctioned workflow and it's the right call for a single lower-third handed to an editor.

It isn't a data pipeline. MOGRTs strip expressions that use unsupported methods, third-party plugins don't survive the export, expressions can break if the person opening the template runs a different language version, and not every property you want is exposable in the first place. The usual workaround is linking to a Slider Control and driving the real property from that, which is more expressions, which is the thing we're already worried about. My deliverable is a rendered video, not a template someone else fills in, so the whole handoff layer is overhead I'd be paying for nothing.

Raw expressions against sourceData, then. Which brings us to the problem.

Where it falls apart

After Effects handles expression-driven text HORRIBLY. Viewport performance is awful, and it's been consistently awful across 2024, 2025, and the 2026 releases. This is not a "wait for the next version" problem. I have waited for three.

Here's the shape of it. Expressions are evaluated per property, per frame. Every text layer bound to your JSON re-runs its expression on every frame it's visible, and the expression starts by asking for sourceData again, walking the object again, and finding its record again. A hundred text layers is a hundred independent evaluations per frame, each one redoing the same lookup, none of them sharing a result with the others.

Adobe has done real work on this. The JavaScript engine is substantially faster than Legacy ExtendScript at render time; Adobe's own figure is up to 5x. In 17.0 they added detection for expressions that don't change over a comp so those evaluate once, and made posterizeTime() expressions calculate once for the whole comp instead of every frame. Those are exactly the right optimizations.

They just don't cover the case I'm in. My expressions are constant over the comp (the JSON doesn't change between frame 1 and frame 300), but they're constant in a way the optimizer apparently doesn't catch, and the moment anything in the chain touches time or a layer's in-point, you're back to per-frame.

Scrubbing goes from instant to a slideshow. RAM preview caches, then invalidates the moment you nudge anything. Selecting a layer takes a beat. The comp isn't heavy in any way that shows up in a render estimate (shapes and text, no plates, no 3D), but the timeline feels like you're dragging 4K through a color pipeline.

Community threads reporting the same collapse with CSV/TSV-linked text go back years and are still open.

Large JSON makes it worse, obviously, but "large" arrives sooner than you'd think when every layer walks the whole tree. sourceRectAtTime(), for auto-sizing a background plate behind copy you can't predict the width of, adds to it too, though honestly that one I find tolerable. It's the sheer count of text layers that kills it.

What I've tried

Two things that look like they should work, and don't:

posterizeTime(0). Documented to freeze evaluation, and documented since 17.0 to calculate once per comp rather than per frame. It's the one built-in lever aimed squarely at this problem, and in my templates it did not help. Same scrub, same slideshow.

A hub "DATA" layer. Parse the JSON once on a single layer, hold it in that layer's sourceText as a string, and have every other layer pull it via thisComp.layer("DATA") and JSON.parse() at valueAtTime(0). In theory one parse instead of a hundred. In practice, no difference I could measure. It moved the work around without removing it.

Then there's the standard advice, which is real but is a workaround, not a fix. Precompose and pre-render the text once copy is locked, so it stops being data and starts being pixels. Toggle expressions off globally while you're doing layout and timing. Split the JSON per comp so each expression walks a smaller tree. All three give you a responsive timeline. All three do it by taking away the live-data behavior you built the system for, which is the trade sitting under every mitigation in this post.

What I want to try next

Stop using live expressions entirely. Drive the project from outside: a script (ExtendScript, CEP, or a UXP panel on newer builds) reads the JSON once, writes the strings into the layers as static values, and exits. Or go further and generate the whole project from a Node or Python step. Either way the render sees a normal After Effects project with no expressions in it, because the binding already happened before the app ever opened the comp.

I haven't built this. It's the experiment I keep meaning to run, and I want to be clear about the part that gives me pause: baked values have no live link back to anything. Right now the project is the truth and the JSON is the truth simultaneously, which is slow but self-consistent. Bake it and the comp holds stale copies. Every edit means re-running the script, and now I need to know which layers changed and which ones somebody nudged by hand in between. The AE project isn't diffable, so I'd be inventing a change-tracking story I currently get for free.

That's a real cost, not a hypothetical one. But it trades a problem I can't solve for a problem I can, and I'd rather be managing my own bookkeeping than waiting on a scrub.

Verdict

Data-driven After Effects works. I use it. It has saved me real days of copy revisions and caught errors a manual pass would have shipped.

But nothing I have tried fixes the viewport. posterizeTime(0) didn't. Caching through a hub layer didn't. The things that do help all help by turning the data off (pre-render it, freeze it, toggle it away), which means every working mitigation costs you the exact feature you built the system to get. That's not a workflow problem I can optimize my way out of. It's the runtime, and the runtime is Adobe's to fix.

What I'd want, in order: cached expression evaluation that actually holds across frames for expressions that provably don't vary with time, and a real data binding layer: a first-class link between a property and a key in a data file, resolved by the application instead of by a JavaScript expression re-run 1,800 times per render. sourceData was a great feature to ship in 2018. It has never been given the runtime it needs.

Until then, the honest advice is: expressions for motion, data from outside the comp if you can manage the bookkeeping, and go in knowing the preview will fight you.

Not because I've given up on it. I want JSON-driven AE to work badly, and I keep building it anyway.

RJ
Written by

Ryan Johnson

Motion director and technical creative. 12+ years turning complex ideas into moving pictures.

Keep readingWorkflow · 9 min readThe Render Passes I Actually UseMy real Cinema 4D and Octane pass list for After Effects compositing, why it's short, and what each pass buys you when the notes come in.Tutorial · 10 min readCreating Smooth Easing Curves in After EffectsA comprehensive guide to understanding and implementing professional easing curves that bring life and polish to your motion design work.Essay · 6 min readThe Last Creative in the RoomTen years of arguing with other creatives, then a job where I'm the only one holding the craft side. What that trade actually costs.
Contact

Have a project in mind?
Let's talk.

Motion, video, 3D — end to end.

© 2026 Ryan JohnsonRyanjohnson.io — Series 2026USA · Remote