Lazy HTML evaluation

June 8th, 2011. Tagged: (x)HTML(5), JavaScript, mobile, performance

#7 This post is part of the Velocity countdown series. Stay tuned for the articles to come.

Some time ago Google talked about using a sort of lazy JavaScript evaluation which especially helps mobile devices. The idea was to comment out a chunk of JavaScript you don't need right away and serve it this way. Later, when you need it, you get the content of the commented code and eval() it. More here and here.

At the last Fronteers conference I had the pleasure of chatting with Sergey Chikuyonok, who is so great and (among other things) is responsible for coming up with zen coding and writing a bunch of deep articles on image optimization for Smashing Magazine. So he told me he experimented with similar lazy HTML evaluation and it proved to be incredibly helpful for mobile devices. Not only the overall experience is faster but the initial rendering happens sooner and we all know how important that is.

Sergey is a busy person and chances of him writing about his experiment in English seemed pretty low at the time, so I decided to do an experiment on my own and see what happens. Meanwhile he did write about it so I forgot all about my findings, but here they are now.

Long document

I took one big HTML document - The adventures of Sherlock Holmes, which is half a megabyte or about 200K gzipped. Page A is the document as-is, plus some JS for measurements.

Page B (lazy) is the same page but with about 95% of its content commented out. The remaining 5% is a whole chapter so there's plenty of time to deal with the rest while the user is reading. After onload and a 0-timeout I take the commented markup (conveniently placed in <div id="lazy-daze">) and strip the comments. Then take the "unwrapped" time after another 0-timeout to let the browser repaint the DOM and regain control.

The overall skeleton of the lazy page is like so:

<!doctype html>
 
<html>
<body>
  <h1>THE ADVENTURES OF<br/>
  SHERLOCK HOLMES</h1>
  ...
  ... to chat this little matter over with you.</p>
 
  <div id="lazy-daze">
  <!--
    <p>II.</p>
    <p>
    At three o’clock precisely ... 
    ... she has met with considerable success.</p>
  -->
  </div>
 
<script>
 
 
window.onload = function () {
 
    setTimeout(function(){
 
        var daze = document.getElementById('lazy-daze'),
            inner = daze.innerHTML;
 
        daze.innerHTML = inner.substring(4, inner.length - 4);
    
        setTimeout(function(){
            // take end time... 
        }, 0);
                
    }, 0);
};
 
</script>
</body></html>

Experiment

All the files are here:
http://www.phpied.com/files/lazyhtml/

We have the plain normal document - http://www.phpied.com/files/lazyhtml/sherlock-plain.html
And the lazy one - http://www.phpied.com/files/lazyhtml/sherlock-lazy.html

In order to run the experiment you just go to
http://www.phpied.com/files/lazyhtml/start.html
And click "Go nuts". This will load each of the two documents 20 times and take a few time measurements. "Go nuts" again and you'll get 20 more data points.

The time measurements I take are:

  • "plain" - unload to onload of the base version
  • "lazy" - unload to onload of the lazy version NOT including unwrapping it. This should be quicker than the plain version
  • "unwrapped" - unload to onload plus time to unwrap and rerender - this is expected to be bigger than "plain" because the browser has to render twice and is therefore doing more work
  • DOM loaded "plain" - unload to DOMContentLoaded instead of onload
  • DOM loaded "lazy"

Then I take the same 5 measurements but instead of starting at unload of the previous page, it starts at the top of the documents, as soon as a timestamp can be taken with JavaScript. This will exclude DNS, establishing connection, time to first byte...

Results

Here are the results from back when I did the experiment originally last year, using iPhone 2 (with iOS 3.2 or thereabouts)

I ran this experiment over Wifi and again over 3G.

First striking thing - it takes the about the same time to load the plain old page over Wifi and over 3G. For the smaller, "lazy" document, there is a difference, but there's virtually none for the plain base page. The guess here is that the rendering and its cost in terms of memory and CPU is far greater than the actual download time. In other words it takes longer to render than it does to download an HTML. At least in this class of phones. This guess is confirmed when you look at the time from the top of the documents, when the request overhead is removed:

With or without the request time - it's all pretty much the same.

The next striking thing - and how about that lazy document! It renders 3-4 times faster than the whole plain document. Not bad.

And one more surprise - lazy+unwrap time is less than the plain old document. Now that's interesting. It appears faster to split the task into two and do the whole double-rendering, which should've been slower because it's extra work. I guess that poor phone really chokes on the long document.

The same I found is true in Firefox, but almost the difference is negligible.

iPhone 4

I repeated the experiment tonight on iPhone 4 and wifi. And boy, is there a difference. What used to take 13 seconds is now under 3s.

The lazy + unwrap time is more than the plain time, which was to be expected.

Rendering that initial lazy document is still 2-3 times faster that waiting for the whole document.

The numbers:

  • 2765 plain (2014 DOM)
  • 1268 lazy
  • 2995 lazy+unwrap

Ignoring the request overhead:

  • 2200 plain (1421 DOM)
  • 715 lazy
  • 2423 lazy+unwrap

And one last run/observation - on the 3G and iPhone 4 there isn't much benefit of lazy-evaluation and empty cache. The request seems much more expensive. unload to onload 4.9s where document top to onload is 2.5. When the request overhead is out of the picture than lazy eval wins again - 1.7s compared to 2.5s

Parting words

  • Lazy HTML FTW?
  • Who the heck loads an entire book in a page?! Well it may happen. It may not be a whole book, but just a lot of markup. The entire book gzipped was 219K. A hefty document, but have you seen some of those news sites?
  • Possible use case - blog comments. Lots and lots of blog comments. Or posts.
  • If you're going to lazy-load something and get it with an ajax request, why not save yourself the request and ship with another chunk of html
  • This was a simple layout task. Just a bunch of text. I'm guessing there could be much more complicated pages and layouts to render. And rendering is what takes the time it seems.
  • Drawbacks a plenty because of the hidden content - accessibility, SEO.

Thoughts? Anyone want to run the test on Android or any other phone/device/tab/pad/whathaveyou? The guess is that the newer/powerful the device the smaller the difference. But it will be nice to know.

Tell your friends about this post: Facebook, Twitter, Google+

21 Responses

  1. Nice experimentation!

    Even if it’s obvious, you should tell readers that JS is mandatory. I suspect you put this in the accessibility drawback, which is somewhat wrong.

    So as for lazy Ajax loading, we need a first page without anything lazy that puts a cookie, this cookie being later used on server side to add comments around the lazy content.

  2. How about if you put all the commented out stuff in “” and then just removed the style attribute – much less DOM manipulation for the engine – wonder if it would run just as fast or maybe quicker (ie is it the parsing or the notional rendering that costs) ?

    And the text would still be there for accessibility etc – you could even get fancy and do it with a bit of embedded CSS and have a class that’s specified “display:none” just for the handheld media types, and the javascript simply removes the class – but if would still be displayed for other media types.


    Tim

  3. Arghh… wordpress saw my tag suggestion as markup – first line should read
    stuff in <div style=’display:none’> and then just removed the style attribute

  4. Another factor to consider is that you’re still sucking the user’s bandwidth for content they probably won’t need in this current viewing. It’s an interesting experiment, but I think you’d get better mileage with ajax-enhanced pagination and an appcache manifest.

  5. …though of course going the app cache route is still going to mean eating the user’s bandwidth. Never mind me :)

  6. Great post… I’m going to blog about this (http://www.3pmobile.com/blog/) because the performance improvements are dramatic!

    Here are the numbers from my HTC Sprint phone. However what you can’t see from the numbers is how fast the page actually loaded in the browser. We have an Android app that tells you that in real time (also tells you real time geo-location, carrier/network info and device capabilities)

    The results are amazing. Lazy is virtually 50% faster than plain. In fact over Wi-Fi I was able to load the Lazy page in sub 2 seconds which is incredibly fast for 1/2 MB of data! In fact it’s stunning.

    @ Steve W – if you want to create the page I’ll run the tests and we can prove if it’s faster or not.

    Sprint 3G Wi-Fi

    Plain 4595 5589
    DOM loaded plain 3313 2265
    Lazy 4343 2136
    Unwrapped 7732 6763
    DOM loaded lazy 4335 2011
    Plain/no request 3177 4372
    DOM Loaded plain/no request 1895 1048
    Lazy/no request 2045 834
    Unwrapped/no request 5434 5461
    DOM loaded lzy/no req 2037 709

  7. thanks for tips…but where the file to upload in server or page ?

  8. [...] – Lazy HTML evaluation #6 – Preload in visual search [...]

  9. “daze.innerHTML = inner.substring(4, inner.length – 4);”

    Why are you not stripping out the closing comment? Surely it should be “inner.length – 7″?

  10. This can be used for a plethora of options. It even allows for passing sites through firewalls and prevent statueful inspection… Nice loophole.
    It seems that this would even speed up things through proxies also, since the validation of the HTML does not take place (it’s comments after all)

    I can see some uses (evil ones) to this.
    Besides, it looks like FireFox is loading the page faster too…

  11. @Richard-
    It’s the difference between `substr()` and `substring()`. `substr()` has as its second param a number of characters. `substring()` by constrast has the index of the character to stop at. So, `substring(4,length-4)` will in fact go from index 4 to the index 3 positions from the end, which is what is desired. `substr(4,length-7)` would ostensibly do the same thing.

  12. Here’s the data for a T-Mobile G2 (overclocked to 1.5 Ghz) running Android’s default browser on a 3G network:

    plain: 5218 ,4955,5218,6856,5644,7477,4528,5198,6818,6849,4410,4996
    DOM loaded plain: 5157 ,4757,5214,6841,5640,7395,2579,3310,6812,5157,1704,1899
    lazy: 2089 ,2089,1633,1460,2087,3904,2361,1724,2936,3467,2577,1477
    unwrapped: 5565 ,4823,5565,5379,6362,8156,4895,6610,5869,6812,5496,4887
    DOM loaded lazy: 2074 ,2074,1618,1445,2074,3889,2343,1710,2928,3448,2555,1458
    plain/no request: 3214 ,2120,3098,2779,2866,3214,2823,4387,3539,3848,3713,4282
    DOM loaded plain/no req: 2499 ,1922,3094,2764,2862,3132,874,2499,3533,2156,1007,1185
    lazy/no request: 785 ,724,813,686,894,1039,1058,1047,581,633,785,739
    unwrapped/no request: 4149 ,3458,4745,4605,5169,5291,3592,5933,3514,3978,3704,4149
    DOM loaded lazy/no req: 763 ,709,798,671,881,1024,1040,1033,573,614,763,720

  13. [...] http://www.phpied.com/lazy-html-evaluation/ [...]

  14. Be careful with transporting actual data inside of comments – you might be in for a surprise when you do so and then (some day decide to) deliver your document to the client as XML: As we all know, an XML parser is allowed to THROW AWAY any comments …

  15. [...] Lazy HTML evaluation [...]

  16. [...] [00:06:35] Lazy HTML Evaluation [...]

  17. [...] Lazy HTML Evaluation – Performance-Trick für schrittweises [...]

  18. No obvious difference on opera 11.50 desktop for windows xp

  19. [...] Lazy HTML evaluation [...]

  20. It’s the difference between `substr()` and `substring()`. `substr()` has as its second param a number of characters. `substring()` by constrast has the index of the character to stop at.

  21. Thx for sharing this!

Leave a Reply