Not sure when it started happening, but Twitter on my browser is really slow. By slow, I mean the UI responds slowly. Specifically, when I type in the tweet textarea, each letter takes about a second to appear. Even after I finish typing the letters take some time to catch up. At first I thought maybe all those hours on typing tutor really paid off. But, no: the browser was unresponsive and CPU activity soared. Something weird was happening in the Javascript.
Short version: a fix in the form of a bookmarklet. Drag the following link into your bookmarks, and use it on your Twitter page.
I'm using Camino Version 1.6.7 (1.8.1.21 2009032711). Camino uses Mozilla's Gecko layout engine, which is the same engine Firefox uses. But, unlike Firefox, Camino's widgets and UI use Cocoa and not XUL. Sharing the same engine means that if I'm having problems with Camino, it's likely someone has or is having the same problem with Firefox.
Anyway, my first guess was that updating the character counter was related. I tried to find the script that was updating it as I pressed keys. I noticed that when the page loads, the character count only appears after the last tweet is displayed. I guess there must be some AJAX happening that updates the last tweet, and initializes the character counter.
I started by setting a breakpoint on some script line on the index page, then reloading. I stepped through the code and noticed that it was calling some things in application.js. I copied the obfuscated script, transformed it with a beautifier, then pasted it into vim.
There was roughly a thousand lines of code in there. I started with a search for the ids of the textarea or character counter: #status and #status-field-char-counter. No such luck.
I must have scanned through the script and accidentally found isCharCounter. At a glance it looked like what I was searching for.
$.fn.isCharCounter = function()...
var J = F.parents("form");
var E = J.find(".char-counter");
I tidied up the function to what was essentially the same thing, but without that weird nested if thing going on.
After tidying up and reverse refactoring the various G() and C() functions, I had something I could test that did the same things (mostly):
- updated the character count
- changed the color of the count
- enabled/disabled the submit button
Replaced
if (K <= 0) {
E.css("color", "#cccccc");
C()
} else {
[...snip...]
if (K > 130) {
E.css("color", "#d40d12")
} else {
if (K > 120) {
E.css("color", "#5c0002")
} else {
E.css("color", "#cccccc")
}
}
}
}
with
function BetterIsCharCounter...
if (len > 0 && btn.disabled) {
btn.removeAttr("disabled").removeClass("disabled");
} else if (len == 0) {
btn.attr("disabled", "disabled").addClass("disabled");
}
if (len <= 120) { counter.css("color", "#cccccc") }
else if (len <= 130) { counter.css("color", "#5c0002"); }
else { counter.css("color", "#d40d12"); }
Also, it had the same slowness problem.
At this point, I had a few guesses as to the source of the issue:
- The function was
binded to the textarea listening for blur, focus, change, paste, and input events. Maybe somehow with each keypress the function was being redundantly called 5 times. - DOM was traversed on each keypress
E.html()was doing something weird
I tested 1. by registering only one event type at a time using the Firebug console.
> t = $("textarea#status")
> t.unbind()
> t.bind("blur", function(k) { BetterIsCharCounter(); })
Well, to no one's surprise, the issue appeared only when the textarea was listening for input events. input events are fired with each keypress.
So I thought, maybe input was just a slow event.
> var i = 0
> t.unbind()
> t.bind("input", function(k) { i++; })
Typed some nonsense. The UI was responsive, and console.info(i) printed 12. The events were being handled. So, something was making the function slow. I surrounded everything with console.time and console.timeEnd, but each of the lines ran under 2ms. That's just not enough time to be causing the amount of lag.
I was quite sure it had to do with the line which updates the counter even though the timer read 2ms. I tried:
> var i = 0
> t.unbind()
> t.bind("input", function(k) { E.html("" + (i++) });
And, the keystrokes were slow again. Why did the timer's come back with only 2ms when there had to be at least a 300ms lag?
I didn't dig much, but my guess is that even if E.html returns immediately, when the browser updates the layout, it locks the Javascript thread for the length of the rendering time.
I tried various ways to update the DOM:
> t.bind("input", function(k) { setTimeout(function() { counter.html("" + (i++)) }, 10); })
> e = $("#status-field-char-counter")
> t.bind("input", function(k) { e.innerHTML("" + (i++)) })
> t.bind("input", function(k) { e.replaceChild(document.createTextNode((i++).toString()), e.childNodes.first) })
But, every way resulted in the same unresponsiveness.
Instead of updating the DOM on every “input” event, perhaps the best way is to update the DOM only once: after the last keystroke. Instead of updating the DOM, we set a timeout to wait 300ms. If no event occurs during this delay, we can update the DOM.
Replace:
E.html("" + (i++));
with
if (timeout) clearTimeout(timeout);
timeout = setTimeout(function() { counter.html("" + (i++)); timeout = false; }, 300);
There's probably a way to do this leveraging jQuery. But, for now, here is what I've come up with.
(function() {
var btn = $("input#update-submit");
var counter = $("#status-field-char-counter");
var txt = $("textarea#status");
var timeout = false;
var updateFn = function(len) {
counter.html("" + (140 - len));
if (len > 0 && btn.attr("disabled")) {
btn.removeAttr("disabled").removeClass("disabled");
} else if (len == 0) {
btn.attr("disabled", "disabled").addClass("disabled");
}
if (len <= 120) {
counter.css("color", "#cccccc")
} else if (len <= 130) {
counter.css("color", "#5c0002");
} else {
counter.css("color", "#d40d12");
}
};
txt.unbind("input");
txt.bind("input", function(k) {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(function() {
updateFn(txt.val().length);
timeout = false;
}, 300);
});
})();

Comments
Good job on the research for
Good job on the research for this script!
It looks like its helping, BUT there still seems to be a small amount of lag. Something else might be going on…
Great leg work on this
Great leg work on this issue, I hope twitter take notice!
I just enormously sped up
I just enormously sped up Facebook in the Mozilla web browser by disabling pre-fetching of pages when idle. This is done by setting “network.prefetch-next” to “false” in about:config.
Apparently Facebook's Javascript makes lots of idle calls, ordinarily a good practice. Twitter's may do the same.
This performance problem has been driving me crazy.