|Posted by kyle on January 6, 2009 1:59 AM||bookmark / share:|
Recently I've been speaking to colleagues about progressive script execution, and it seems like a little-explored topic, so I'm sharing what I know. By progressive execution, I mean that scripts begin executing in the browser before the containing resource has been wholly received.Dealing with Dynamic Response Sizes
One of the first hurdles people encounter with progressive response handling is that most HTTP responses have a Content-Length in the response header. This means that the size of the response must be known before any data is sent over the network. For static resources, this isn't such a problem because the entire response is available at once, but for dynamic resources, this means that the entire response must be determined before any part of it can be written to the network.
Imagine your web application performs an expensive operation that takes up to half a second to complete. If it takes another 500ms on average to transfer all of your HTML and CSS to users, it will take a full second to load your page. If you could transfer the bulk of that data while waiting for the operation to complete, then tack on the final bits, you might cut your response time in half.
This can be accomplished with effective use of HTTP Chunking ("Transfer-Encoding: chunked"). Chunked responses don't require a Content-Length header, so it's possible to do work after bytes have been sent to the network. While each chunk in the response will contain a header, indicating the size of the chunk, any number of chunks can be appended until a terminating delimiter is provided.Executing Scripts Progressively
In the hypothetical example above, we were dealing with HTML and CSS, but suppose instead that we have a large amount of script to deliver with each response. Take note, external <script>s and XHRs are not progressively interpreted*. Other methods, such as inline <script>s and iframes are required. Also note, individual script blocks are not evaluated until the full contents have been received and parsed by the client. So it's advisable to split scripts into small sections, each wrapped in it's own block:
<script> ... </script> <script> ... </script> <script> ... </script>
The ideal size of these script blocks will vary by application and the connection characteristics of your users. You might consider typical packet size (about 1460b) and the behavior of TCP Slow Start as a guide.Browser Support and Caveats
To explore progressive execution, I implemented a simple HTTP server in Perl (View Source for Example #1), that writes several small chunks (real HTTP chunks) that look like:
<script>document.write('10: 737b: ' + n() + 'ms<br>')</script>
The fields indicate the chunk number, number of bytes written to the buffer, and a function call that outputs the milliseconds since the last script execution. I was surprised by two findings:
1) Webkit browsers don't support progressive execution at all. Ouch!
2) Other browsers don't begin parsing and executing until a byte threshhold has been reached, and this threshhold varies from browser-to-browser. In IE7, my output looks like:
0: 127b: 0ms 1: 188b: 0ms 2: 249b: 0ms 3: 310b: 969ms 4: 371b: 1015ms ...
And in Firefox 3:
0: 127b: 2ms 1: 188b: 0ms 2: 249b: 1ms 3: 310b: 1ms 4: 371b: 1ms 5: 432b: 1ms 6: 493b: 0ms 7: 554b: 1ms 8: 615b: 1ms 9: 676b: 1ms 10: 737b: 1ms 11: 799b: 0ms 12: 861b: 1ms 13: 923b: 1ms 14: 985b: 1ms 15: 1047b: 876ms 16: 1110b: 1021ms ...
This shows that the threshhold in IE7 is about 200-300b, and in Firefox3, about 1kb. While scripts won't execute until the threshhold is reached, it may still be advantageous to deliver content smaller than the threshhold early to take advantage of idle network time. Though you might consider padding your response if it enables you to provide early feedback to users.
It's not necessary to create a new chunk for each script block to achieve progressive execution, but you must be able to determine the size of each chunk before it can be written. It's also important to flush the network buffer at logical intervals (at the close of each script block, for example).
A modified version of the demo HTTP server (View Source for Example #2) demonstrates that progressive execution can be achieved within chunks. For this example, each chunk contains 5 script blocks. I precomputed the total size of each chunk, and appended the output in 1 second intervals. Output for a run in IE follows:
0: 127b: 0ms 1: 188b: 0ms 2: 249b: 0ms 3: 310b: 1016ms 4: 371b: 922ms ...
And for Firefox 3:
0: 127b: 1ms 1: 188b: 1ms 2: 249b: 1ms 3: 310b: 1ms 4: 371b: 1ms 5: 432b: 1ms 6: 493b: 1ms 7: 554b: 1ms 8: 615b: 2ms 9: 676b: 1ms 10: 737b: 1ms 11: 799b: 1ms 12: 861b: 1ms 13: 923b: 1ms 14: 985b: 1ms 15: 1047b: 1136ms 16: 1110b: 817ms ...
That concludes my overview. I'm interested in questions and ideas for further exploration.