At work, we have a web application based on some common technologies: ASP.NET, MVC, and jQuery. One of the functions of the software is to provide data reports to clients in the form of charts, tables, and exportable documents. Recently, it was noted that under some conditions (long reports), a report dialog would appear to freeze. This behavior only occurred in Chrome browser, any version, but not in Firefox or IE.
Luckily, Chrome has excellent developer tools built in. They have become my starting point nearly every time I debug a web app. IE and Firefox have their own similar tools, accessible with the F12 hotkey or through menus. Typically there is a document inspector for the current page (allowing you to view source for individual elements), a network traffic analyzer, a profiler, and a script debugger. Each of these tools is invaluable when debugging web apps. If you wrote the web app yourself, you may already know exactly where to look in the code, but for the sake of this article let’s pretend you did not write it.
Why not use Visual Studio in this case, you might wonder? From what I’ve seen, Razor (what MS calls its technology for dynamically generating HTML from .NET) support isn’t great in the VS debugger, at least in the version we use. Especially when you involve the remote debugger, extraspecially when the remote debugger is working across different domains. You can spend hours trying to figure out how to get your debugging tools to work and maybe never find an answer, or you can use what simply works, which do you prefer?
The first step was to open the Chrome dev tools with F12 and load up the web app. Following the provided steps, I was able to reproduce the bug: when closing the report dialog, the tab froze and had to be forced to close. The easy part done. To discover what it was doing (it must be doing something, to appear unresponsive), I used the CPU profiler tool. I intended to let it run for several minutes, but to my surprise, the dialog closed after ~45 seconds. Normally, it’s less than 1 second, and indeed it takes maybe 2 seconds in IE and FF. What’s going on with Chrome?
I opened my trusty profiler and examined the call tree. Most of the time was spent in the event handler for closing the dialog, no surprise there. But inside of that was the jQuery function remove() and a function compareDocumentPosition. Clearly, it was doing some very slow sort operations, something that performs terribly in Chrome.
Two more little secrets that help when debugging web apps: sourceURL / source maps, and JSFiddle. sourceURL is a special comment that can be used to name a JS source, allowing it to show up in Chrome’s debugger:
//#sourceURL=foo.js
Our app had no such definitions at first, but it became much easier to debug after adding a few (the actual source files are Razor CSHTML). JSFiddle seems like every web dev’s secret weapon, letting you test code with a variety of different frameworks and APIs – perfect for isolating a bug like this.
This is when I start thinking about bug reports to Chrome and/or jQuery, because the same code works fine in other browsers. All I need is a minimal test case (preferably with latest versions) so the developers of those projects can look into it. And this proved to be quite a challenge. Trivial tests didn’t reproduce the issue at all. A jQueryUI dialog with a lot of Google Charts data stuffed into it closes with no problem. There must be something else.
It’s time to look a bit more closely at what’s going on. How do jQuery UI dialogs work, how does jQuery work for that matter? Well, basically all they do is wrap native document APIs, in this case it’s HTML. They can add and remove elements from the page, and a lot more, but this is what’s relevant here. If you think about a dialog in a desktop app, you think of a separate “window” opening or something similar. In a web page, you have one “window” so to speak. Showing and hiding a dialog involves modifying the HTML on the page somehow. Open the dev tools document inspector on this page and you can see it changing.
What is it about our web app that is triggering this bug… maybe it’s the amount of HTML on the page? Google Charts spews out all kinds of stuff to render it’s interactive images. If the data source has many points, there will be that many more elements output on the page. In fact, this was observed, the size of the page increased by roughly 4x under the reported conditions, even before the dialog was opened. It makes intuitive sense if a function that manipulates a document performs more slowly when the document explodes in complexity (in this case, I would say not a linear relation, it’s not 4x slower, but more like 40x slower).
My next thought was if jQuery is only a wrapper for native functions, what is the equivalent of remove() and can it be replaced? It can, as this very helpful article explains. I only needed to find how to get the native object from the jQuery object in question. That turns out to be trivial, it’s the first element of an array. Getting the correct native object from the jQuery UI dialog object, however, proved to be a little more complicated. You need to use the widget method. So what I ended up doing was replacing:
dlg.remove();
with:
var nativeDlg = dlg.dialog('widget')[0]; nativeDlg.parentNode.removeChild(nativeDlg);
A little yuckier, isn’t it? But guess what, it works! Somehow, remove() caused the dialog to close 40x more slowly in Chrome than it does with native methods.
At the time of writing this post, a test case hasn’t been fully developed for this yet, by me anyway. This is merely a workaround, but I wanted to show some practical techniques for debugging performance issues, or really any issues, in a web application.