Devblog: Full Google Mail in a clean room browser

Earlier in the year, we spent a pleasingly small amount of time making Google Mail’s basic HTML version run inside Flow. I thought it’d be a fun target to get the full version working since there was no obvious design or feature limitation stopping it, just bugs or missing JavaScript APIs.

GMail in a clean room browser

We have created a clean room browser engine and are working on extending its features, so it’s useful to have little projects to focus on. It was clear that full Gmail made use of all sorts of recent HTML features including web fonts, flexbox and transitions, all of which we supported, but it wasn’t immediately obvious which others would be required.

Google’s JavaScript code is impressively large and obfuscated. This made it very tricky to see what all the problems were, but the console usually gave clues when exceptions were thrown. Other problems, such as weirdness with mouse highlights, were tracked down through educated guesses and finding or writing test cases to prove them. 

Over the course of getting Gmail working, Flow has had a number of bugs fixed and has gained various features, such as Mutation Observers, XMLSerializer, ‘contenteditable’, and HTML <template> element. This all means that our browser engine is likely to render more HTML correctly first-time.

I’ll be the first to admit it’s not perfect yet – some vertical alignment is wrong in a few places, Hangouts doesn’t work, and our ‘contenteditable’ support is minimal. But I don’t think these detract from the achievement – as far as I’m aware, no browser engine other than Webkit, Blink and Gecko can run full Google Mail, and these have all had over twenty years of development work on them.

A detailed look 

There were three main areas we needed to focus on – the modern Google login box, Gmail itself, and Hangouts. Helpfully, these could be debugged in parallel since Google’s support site has a special URL to access the full Gmail without doing any browser checks. Given I could log in using the legacy dialogue I could make a start on the full site at the same time as someone else was looking at the modern login process.

The first problem was easy to identify – a white screen, but the console (correctly) displayed ‘window.postMessage is not a function’ every time Gmail was loaded. That was a good chunk of work which also involved updating our event loop to make it closer to the current HTML specification, and making various fixes to our serialising and deserialising code.

The second problem was much less obvious. An obfuscated function name was often reported as not a function, but other times not. I soon learnt that while external scripts can be fetched asynchronously, they must still be executed in DOM order. I then had another script-related problem; an assert, but pleasingly the only one in this whole task. Scripts appended to the tree should be executed immediately but, it turns out, a little less immediately than we were doing. In this case, a small node tree was being appended with children including a script element. Flow was stepping through the nodes setting various flags and executing scripts, but that meant nodes after the script element had incoherent flags. That script was accessing these future nodes and asserting.

Pleasingly, after that Gmail rendered almost perfectly but, annoyingly, there were still more bugs and missing features left in order for it to be actually usable.

The most obvious problem was with the mouse highlights being a bit random. Moving the mouse up and down the list of emails correctly highlighted them, but just as you tried to click on the helper icons they disappeared. This turned out to be a simple mouse enter/over leave/out ordering problem, tracked down by finding a test case and playing around in different browsers. The spec is quite clear as to the order of these mouse events, though I was a bit surprised that a website needed to use all possible events.

We also hadn’t implemented MouseEvent.relatedTarget, which was a bit of a surprise. We have had it internally forever to correctly dispatch the mouse events above, but I must have forgotten to expose it to JavaScript, and no sites I’d tried seemed to need it.

Though the highlight now seemed to work, moving the mouse over each email in the inbox displayed an exception ‘cells is not a function’ on the console. We had no JavaScript APIs available on our HTMLTableElement class or any table-related elements, so those were added.

While I could click on individual emails, clicking on an email thread caused an infinite loop. That turned out to be a very lazy implementation of getElementsByClassName() that returned a non-live list, rather than live. It doesn’t seem to be a performance problem for Gmail, so for now, it returns an inefficient live list, which can be improved once we have a need.

There was an impressive performance problem on load where it took over a minute to get to the inbox. This turned out to be a multi-megabyte string being passed through the HTML parser via innerHTML. We were incrementing the allocated string one byte at a time. Changing that to 1024 bytes made it load essentially instantly.

All seemed fine with reading emails, but we hit some big problems with composing emails. It was immediately obvious that we needed to implement ‘contenteditable’, so that was started in parallel, and while it isn’t yet complete (the cursor can currently only be at the end of a line), it’s functional enough to write simple emails.

The ‘Compose’ button was also very temperamental, usually not working. The console told me that Mutation Observers were required, which weren’t too tricky given the level of detail in the whatwg specs. That seemed to fix the ‘Compose’ button, but then we hit the most frustrating problem of the whole task – clicking on the ‘Send’ button just displayed a yellow ‘Loading’ popup but nothing appeared on the console.

Given Gmail is very large and obfuscated, it wasn’t practical to understand what the JavaScript was doing and so, after spending a very tedious time breakpointing, I resorted to searching the source for keywords that matched browser APIs. JavaScript that calls a browser API can’t easily obfuscate that line since the browser needs to recognise the call. Gmail clearly used HTML templates, although it appeared there were fallbacks if they weren’t supported. Knowing that all modern browsers support templates, I wasn’t convinced that these fallbacks had been tested in a long time so I added template support. Not totally unsurprisingly, there was no change.

Then NodeFilter showed up in my search, together with Document.createNodeIterator() and Document.createTreeWalker(). Again, pretty simple to add, but this also made no difference. I still like to think Gmail was getting further, although I haven’t bothered to investigate to avoid disappointment. But, I do know that adding node iterators and tree walkers makes render its news articles.

Finally, a breakthrough; by luck, a colleague managed to get an exception on the console that I’d never had – ‘XMLSerializer is not defined’. Breakpointing in Safari showed it was called every time ‘Send’ was clicked. I made a start creating the XMLSerializer class and a dummy method, and then sending worked!

There’s still a bit left to polish off. Hangouts, on the left, doesn’t yet work and some of the icons are aligned slightly incorrectly, but all the complex internal functionality appears to work fine.

If you’d like to automatically receive our posts by email please join our mailing list.