Speed Up Your Dart App's Initial Load With This Transformer
I just saved potentially hundreds of blocking milliseconds from my Dart app's initial loads.
By moving script tags, and using my new pub transformer, you can help your users, too.
Where does the time go?
First, some background:Typical Dart applications are designed to work in Dart VM and modern JavaScript. A small file, named dart.js, is loaded by the HTML file and checks to see what runtime is available. If the Dart VM is available, the Dart code is run. Otherwise, the dart.js file swaps out the Dart script for its equivalent JavaScript file compiled from the original Dart code.
Dart Editor can generate skeleton applications, complete with starter HTML, CSS, and Dart files. It's really handy for getting started quickly. As of today, the default starter HTML code looks something like this:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hugs all around</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
.......
<!-- not a good practice, do not copy -->
<script type="application/dart" src="hugs.dart"></script>
<script data-pub-inline="packages/browser/dart.js">
</body>
</html>
Some browsers delay script execution until CSS resources are downloaded (unless the script tags are before the CSS file). This means that the dart.js file isn't executed until the CSS file in the <head> is downloaded. This is an unnecessary delay in app startup.
Location and order matters for scripts and CSS resources. Default projects from Dart Editor can be optimized.
For the above HTML file, the browser's workflow looks something like this:
- The main HTML loads, with links to CSS, dart.js, and the Dart application.
- The browser downloads the dart.js file and the CSS file, more or less concurrently.
- The CSS file must finish downloading until the dart.js script is executed.
- dart.js checks to see if Dart VM is available. It's probably not.
- dart.js looks for the Dart application script tag.
- dart.js replaces the script tag with another script tag that points to the Dart app compiled to JavaScript.
- The browser then, finally, downloads the actual application.
For my simple app, hosted on the public internet, the app loaded in approximately 1.29 seconds. There are two root causes: the unnecessary download of dart.js, and the blocked execution of dart.js.
The app was blocked for 300ms.
Saving time
I wanted the functionality of dart.js without the extra delay in startup. The solution was to move the script tags, and inline dart.js.
Step one, move the script tags into the <head> tag, before the CSS resource.
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hugs</title>
<script async type="application/dart" src="hugs.dart"></script>
<script async data-pub-inline src="packages/browser/dart.js"></script>
<link rel="stylesheet" href="styles.css">
</head>
Dart apps don't begin until after DOMContentLoaded, so it's safe to start downloading the app as soon as possible (as long as the script tag is marked as async).
Step two, add an async attribute to the script tags. This attribute tells the browser not to block page parsing while the script is downloading and executing.
Step three: write a pub transformer to rewrite the HTML, and inline the contents of dart.js. Therefor, script_inliner, my new pub package and transformer, was born!
Step one, move the script tags into the <head> tag, before the CSS resource.
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hugs</title>
<script async type="application/dart" src="hugs.dart"></script>
<script async data-pub-inline src="packages/browser/dart.js"></script>
<link rel="stylesheet" href="styles.css">
</head>
Dart apps don't begin until after DOMContentLoaded, so it's safe to start downloading the app as soon as possible (as long as the script tag is marked as async).
Step two, add an async attribute to the script tags. This attribute tells the browser not to block page parsing while the script is downloading and executing.
Step three: write a pub transformer to rewrite the HTML, and inline the contents of dart.js. Therefor, script_inliner, my new pub package and transformer, was born!
To install the transformer, add it to your pubspec.yaml dependencies:
dependencies:
script_inliner: ">=0.0.1 <0.1.0"
Then, add script_inliner to the list of transformers in your pubspec.yaml:
transformers:
- script_inliner
Then, add a data-pub-inline attribute to script tags that should be inlined. For example:
<script data-pub-inline src="packages/browser/dart.js"></script>
When you run pub build or pub serve, the transformer kicks in and inlines the script.
The results
Notice how the browser no longer requests dart.js (because the contents were inlined), and the browser downloads the app's script file as soon as any other external resource. After numerous reloads (and clearing the cache), the approximate average time was 916ms.
Please move your script tags and try the script_inliner transformer and let me know if it works for you.
For the curious, here's what the HTML now looks like:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hugs</title>
<script async type="application/dart" src="pub_transformer_test.dart"></script>
<script data-pub-inline="packages/browser/dart.js">
(function() {
if (navigator.userAgent.indexOf('(Dart)') === -1) {
var scripts = document.getElementsByTagName("script");
var length = scripts.length;
for (var i = 0; i < length; ++i) {
if (scripts[i].type == "application/dart") {
if (scripts[i].src && scripts[i].src != '') {
var script = document.createElement('script');
script.async = true;
script.src = scripts[i].src.replace(/\.dart(?=\?|$)/, '.dart.js');
var parent = scripts[i].parentNode;
document.currentScript = script;
parent.replaceChild(script, scripts[i]);
}
}
}
}
})();
</script>
<link rel="stylesheet" href="style.css">
</head>
......
(Fine print: tests from my home machine, over wifi, hitting static files served by Dropbox. Also note: I've barely tested this transformer, and I haven't tried it with too many different scripts. YMMV.)
Use the performance rules that you've learned as a guide, but always use the tools to measure and verify. Performance is a feature, and has direct correlation to customer satisfaction and conversions. Time spent on faster app loads is time well spent.
(Banner from http://www.ayoungertheatre.com/wp-content/uploads/2011/11/pm_web.jpg)
Inlining dart.js, and moving the script tags, resulted in over 25% faster load times.
Please move your script tags and try the script_inliner transformer and let me know if it works for you.
For the curious, here's what the HTML now looks like:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hugs</title>
<script async type="application/dart" src="pub_transformer_test.dart"></script>
<script data-pub-inline="packages/browser/dart.js">
(function() {
if (navigator.userAgent.indexOf('(Dart)') === -1) {
var scripts = document.getElementsByTagName("script");
var length = scripts.length;
for (var i = 0; i < length; ++i) {
if (scripts[i].type == "application/dart") {
if (scripts[i].src && scripts[i].src != '') {
var script = document.createElement('script');
script.async = true;
script.src = scripts[i].src.replace(/\.dart(?=\?|$)/, '.dart.js');
var parent = scripts[i].parentNode;
document.currentScript = script;
parent.replaceChild(script, scripts[i]);
}
}
}
}
})();
</script>
<link rel="stylesheet" href="style.css">
</head>
......
(Fine print: tests from my home machine, over wifi, hitting static files served by Dropbox. Also note: I've barely tested this transformer, and I haven't tried it with too many different scripts. YMMV.)
Measure and optimize
Use the performance rules that you've learned as a guide, but always use the tools to measure and verify. Performance is a feature, and has direct correlation to customer satisfaction and conversions. Time spent on faster app loads is time well spent.
(Banner from http://www.ayoungertheatre.com/wp-content/uploads/2011/11/pm_web.jpg)