Was it worth replacing WordPress's media frame to get Backbone out of the core?
The question: fw.js called Backbone.Model.extend and Backbone.View.extend in exactly two
places. Replacing them looked like an afternoon. Then the modal underneath turned out to be
wp.media.view.MediaFrame — WordPress's media frame, which is Backbone, and which Backbone still
loads for anyway. Is removing it worth the risk?
Context
The measured position was encouraging: Backbone appeared in 12 files, and inside fw.js — the
framework's central admin script — in only two call sites. See
the admin JavaScript layer.
Three facts complicated it:
fw.Modalwas built onwp.media.view.MediaFrame. The modal did not merely use Backbone; it lived inside a Backbone view hierarchy owned by WordPress core.- A Backbone object subscribed to ours. A
wp.media.controller.StatecalledlistenTo(modal, 'change:title'), sofw.Modalhad to remain a Backbone-compatible event target no matter what else changed. - Backbone would still load.
wp_enqueue_media()pulls it in wherever an upload option appears, and the page-builder items use it directly. Removing it fromfw.jsdoes not remove it from the page.
And the blast radius was wide: 33 files reach into modal.frame, and every options modal in
wp-admin goes through this code.
Options considered
- Swap the two call sites only. Cheapest. But the modal stays inside a Backbone media frame, so
the framework's most-used UI still depends on Backbone in the way that matters. It buys the claim
"we don't call
Backbone.*" and little else. - Leave it alone. Defensible: nothing is broken, and the practical benefit is invisible to users while Backbone loads regardless. The cost is that the options modal stays coupled to a WordPress internal that the framework does not control.
- Reimplement the slice of the media frame that is actually used. Removes the coupling for real and puts the modal's behaviour under the framework's own control — at the cost of reproducing a WordPress internal faithfully enough that 33 consumers do not notice.
Decision
Reimplement it, under three constraints that made the risk acceptable:
- The DOM contract is frozen. The replacement emits the same structure and class names
(
.media-modal,.media-modal-backdrop,.media-modal-content,.media-frame,.media-frame-title,.media-frame-content,.media-frame-toolbar,.media-modal-close,.media-toolbar-primary). The framework's stylesheets carry 95 rules on.media-modalalone, and its own draggable / sizing / z-index-stacking code reads the same selectors. - The public API is frozen.
$el,modal.$el,views.get(),content.set(),toolbar.set(),toolbar.selector,state(),open(),close(), and theready/open/close/content:create:mainevents all keep their shapes, so no consumer changes. - A kill switch ships with it.
window.FW_LEGACY_MEDIA_MODAL = true, or?fw-legacy-modal=1on any admin URL, restores the wp.media frame with no deploy.
The Backbone object subscribing to ours was resolved by inverting the direction: the modal now
pushes its title into the frame instead of the frame listening for it. That single change is what
freed fw.Modal from having to remain Backbone-compatible.
What actually broke — the useful part
Every bug was the same species: behaviour Backbone provided implicitly, which no call site mentions and which fails silently.
- The namespace was clobbered.
fw.jsreassignsfwto a fresh object near the top. Loading the new primitives before it — as the dependency graph requires, sincefw.Modal = fw.Class.extend(…)runs at load time — meant they were erased a moment before they were needed. Caught pre-release by loading the files in real order outside a browser. - The
eventshash was never delegated. Views declareevents: { 'submit': 'onSubmit' }and never wire the handler;Backbone.Viewdoes it for them. Without that, the options modal's Save button did nothing — Save works by triggering a hidden submit input, so with no submit listener the click vanished. - The event-map form was unsupported. Eight call sites — every page-builder item among them —
bind as
listenTo(this.modal, { 'open': fn, 'close': fn }). Passing a map instead of(name, callback)meantcallbackwasundefinedand the emitter bailed, binding nothing. Everyoptions-modal:*hook was dead, which surfaced as one missing toolbar button.
Bugs 2 and 3 were invisible in the UI because fwEvents.trigger wraps listeners in try/catch and only
console.errors.
The generalisable lesson: when you replace a framework, the risk is not the API you can see being
called — it is the API the framework was calling on your behalf. Grep for what your code declares
(events, object-form listenTo, defaults), not only for what it invokes.
Why
- It removes a coupling the framework does not control. The options modal's behaviour was previously at the mercy of a WordPress internal that can change between releases.
- The cost was bounded and reversible. Frozen DOM, frozen API, kill switch. Nothing about saved content or option values is touched.
- It is a prerequisite for the builder work. The builder items are the larger Backbone share, and they will hit exactly the same three traps. Meeting them here, in a bounded change with an escape hatch, is much cheaper than meeting them in the canvas.
- It makes the claim honest and checkable. The framework core no longer depends on Backbone: no
Backbone.*calls infw.js, and thefwhandle no longer declares it. Backbone still loads for the media library and the builder items — which is the next step, and worth saying plainly rather than rounding up.
Status: Accepted, shipped in 2.16.11 after end-to-end manual verification of the modal, stacked modals, the toolbar, the page builder, Theme Settings and the Live Editor.
