The staging build printed 61 JQMIGRATE warnings on the first page load of the back office admin, and 23 of them pointed at files I wrote myself. That was a Tuesday in September. Production has been on jQuery 3.1.1 for three weeks now, with the Migrate plugin still loaded on purpose, and I want the shape of this written down before I forget it.
jQuery 3.0 shipped on 9 June. We did not move in June. The admin is around 40,000 lines of jQuery across roughly 90 files, some of it five years old, and the gate was never the library. It was browser support: jQuery 3.0 supports IE9 and up, and if you still need IE6 to 8 you stay on the 1.12 branch. IE8 was 0.4 percent of admin sessions last quarter, all of it from two branch machines that got reimaged in August. That number is why this upgrade happened at all.
We followed the upgrade guide’s order rather than jumping: 1.11 to the last 1.12 release with Migrate 1.x, fix the warnings, drop Migrate, then 3.1.1 with Migrate 3.0. It reads like bureaucracy. It is the difference between one class of breakage at a time and a console you cannot read.
The Deferred rewrite is the whole story
Everything expensive traced back to one change. jQuery.Deferred is now Promises/A+ compatible, and .then() is a different method than it was. An exception thrown inside a .then() callback no longer bubbles up to window.onerror; it becomes the rejection value of the promise that .then() returned. Callbacks are always invoked asynchronously, even on an already resolved Deferred. And a non-thenable return from a rejection handler now becomes a fulfillment value, so the next .then() in the chain runs its success branch, not its failure branch.
That last one is the mean one. We had a chain in the account notes panel where a failed save returned a string from the rejection handler for logging. Under 1.11 that string stayed a rejection. Under 3.0 the next handler treated it as success and the panel told the operator the note had saved. No console error, because the error had been converted into a value and the value looked fine.
The other half is the argument signature. jqXHR gives .then() three arguments on success and three different ones on failure, and Promises/A+ says one value, no context. The guide is blunt about the remedy: if you want the old behaviour, use .done() and .fail(), which were never made compliant. Most of our diff was exactly that, about 70 call sites:
$.ajax({ url: "/admin/account/" + id + "/notes", method: "POST", data: payload })
- .then(function (data, textStatus, jqXHR) {
- renderNotes(data.rows);
- }, function (jqXHR, textStatus, errorThrown) {
- flash(errorThrown);
- });
+ .done(function (data, textStatus, jqXHR) {
+ renderNotes(data.rows);
+ })
+ .fail(function (jqXHR, textStatus, errorThrown) {
+ flash(errorThrown);
+ });
Also gone: the .success(), .error() and .complete() methods on the jqXHR object, deprecated since 1.8. Not the options of the same name, only the methods.
One thing to know if you are starting now. 3.0 had a real hole here: ready handlers were routed through the new compliant Deferreds, there was no way to attach a rejection handler to $(function () {}), and a runtime error inside a ready handler was swallowed. jQuery 3.1.0, on 7 July, fixed that by logging the error and adding jQuery.readyException as an entry point. Inside a Deferred you now get console lines starting with jQuery.Deferred exception: followed by the message. Skip 3.0 and go straight to 3.1.1, released 22 September.
Fractional pixels and three dead aliases
The second category was cheap to fix and tedious to find. .width() and .height() now read getBoundingClientRect instead of offsetWidth, so they can return non-integer values. Our frozen table header compared a measured column width against a cached one with === and then set a pixel value from it. With 197.328 instead of 197 the comparison never matched, and the header redrew on every scroll event. The page did not break. It just got slow in a way nobody could describe.
.load(), .unload() and .error() are removed, deprecated since 1.8, and $(el).on("load", fn) replaces them. $(document).on("ready", fn) is removed too, and it deserved to be: it only fired if you attached it before the browser fired DOMContentLoaded. Our service worker registration script was the one place we had used it. Then .size(), .andSelf(), .context and .selector, all of which Migrate names for you.
Migrate is not a failure state
The uncompressed Migrate 3.0 plugin logs each unique warning once, with a documented cause and remedy per message. The compressed build restores the old behaviour silently. We are still shipping the compressed build to production, which the jQuery team calls acceptable short term and I would call honest. Three of our vendor widgets have not been touched since 2013 and one writes to jQuery.event.props. The plugin comes out when those are replaced, and not before.
Forty thousand lines is not debt because it is old
Twice during this upgrade, somebody proposed stopping and rewriting the admin into a framework instead. I argued against it both times. “We should rewrite” is not a technical position by itself.
The admin works. Four people understand it. Its slowest screen is slow because of a query plan, not because of the DOM. Against that, a rewrite means re-auditing every validation rule in a regulated back office, a second build toolchain, and months where two implementations of the same screen both exist and drift. The jQuery 3 upgrade cost about 30 hours across three weeks, most of it reading warnings and rewriting .then(). A rewrite is not 30 hours. It might be the right call one day, on the strength of a specific problem the current code cannot solve. Fashion is not that problem.
Still unresolved: we have no front end tests, so my only evidence that the 70 rewritten call sites behave is four people clicking through the admin for two afternoons. The account notes bug sat in production for six days. If there is a second one in a screen the operations desk uses monthly, we find it in November.
Sources
- jQuery 3.0 Final Released, 9 June 2016: release date, the Promises/A+ change with the exception and rejection examples, removal of the
.load(),.unload()and.error()aliases, and the slim build. - jQuery Core 3.0 Upgrade Guide: IE9+ browser support, the eight step migration order, fractional
.width()and.height()viagetBoundingClientRect, removal of.on("ready", fn),.size(),.andSelf(),.contextand.selector, and the advice to use.done()and.fail()to keep old argument behaviour. - jQuery 3.1.0 Released, No More Silent Errors, 7 July 2016: the swallowed ready handler errors in 3.0 and the
jQuery.readyExceptionentry point that fixed them. - jQuery 3.1.1 Released, 22 September 2016: the version we are running, and what it fixed.
- jQuery Migrate 3.0 warning messages: the per-message cause and remedy list, the jqXHR
.success,.errorand.completeremovals, andjQuery.event.props. - Promises/A+ specification: the rules jQuery’s
.then()now follows, including single resolution value and no handler context.