On Github lakenen / html5devconf2013
Cameron Lakenen – Box
http://camupod.com/html5devconf2013
Cameron Lakenen
Engineer at Box
Preview and View API on document viewing stratgey in the brwoser
aventures in SVG→
Preview and View API use Crocodoc
world-class document viewer →
[scrolling] Here's an example of the document viewer we've built →
rendering strategy is based on HTML5 →
CSS →
and SVG →
Can't you do everything with HTML + CSS?
HTML5 is great
aside from canvas it's not enough →
why SVG? →
provides some operations not available
clipping, masking, blending, paths →
web standards, just work, portable
text file* compresses, important for mobile →
infinite zooming →
in addition to images and text
draw paths, stokes and fills →
complex graphics operations
clipping, masking, blending transparency groups →
zooming
"scalable vector graphics"
rasterized images
crisp zooming →
So, what about canvas? →
interesting option, more complex to render →
requires JS →
zooming or resizing requires a redraw in JS
interesting projects, PDF.js →
SVG yielded interesting problems, embedding in HTML
several ways to embed →
basic img tag or css background-image →
inline svg
object, iframe, and embed tags →
different implications →
performance and stability
ability to load external assets or modify SVG content on the fly
native browser loading indicator spinning on every emebd →
biggest issue is performance
Some docs very complex => complex SVG
conversion process to reduce complexity * file size and rendering performance
affecting rendering quality. →
lazy-load pages => faster, responsive UX
embedding SVG on the fly => janky
certain methods yield better UI perf →
Memory useage, crashing mobile Safari
not going to dive too deeply into mobile →
perf: common external resources (fonts, styles) →
re-using resources like fonts across svg files
base64 encode these assets into SVG file (we do somewhat)
fonts commonly reused
encoded into each svg file, payload size++ →
modify SVG content before embedding
queryString params for A & A →
<xhtml:link href="stylesheet.css" type="text/css" rel="stylesheet" /> <defs> <image id="Image_8_1_R0zyIp" xlink:href="8.png" /> <image id="Image_10_1_R0zyIp" xlink:href="10.png" /> </defs>
<object type="image/svg+xml" data="page-1.svg"></object>
screencast: doc viewer using embed method that causes spinner
annoying and jarring for the users →
<img src="foo.svg"> and CSS background-image
<object>, <iframe>, or <embed>
So let's talk about the different embed methods! →
First, let's look at the img tag →
SVG is an image format
image tag supports SVG files
even CSS background-image supports SVG →
SVGs loaded via <img> won't fetch external assets
https://developer.mozilla.org/en-US/docs/Web/SVG/SVG_as_an_Image#Restrictions
Unfortunately, no external assets (security) →
Solution: base64-encode all assets into nested data: urls
solution: download with JS
base64 encode into nested data url →
very complex, issues in some browsers
it could be explored further, but probably not worth it →
<img> is difficult at best – let's look at our other options:
<img src="foo.svg">
<object>, <iframe>, or <embed>
img won't work... other options? →
Inline SVG is part of the HTML 5 spec!
IE 9+Inline SVG promising
part of HTML5 spec
works in all modern browsers, IE 9+ →
no true inline svg in JS
viewer.js - pages are loaded dynamically
possible to load a 1000+ page document performantly
true inline SVG (literally HTML+SVG text, parsed at page load) is very fast
dynamically embed with JS requires DOMParser
DOMParser bypasses the threaded html parser => synchronous parse of the SVG content
threaded html parser is not exposed to JS, →
could change soon - two open chrome issues →
This is an example document viewer that uses DOMParser to insert SVG into the DOM inline.
[scrolling] You might not be able to tell, but I'm scrolling right now... performance is pretty bad
This is the same document, but here the SVGs are embedded as iframes.
[scrolling] As you can see, performance is much better
iframe and DOMParser example side-by-side
reload the example, you can see the differences
DOMParser takes the synchronous codepath: parsed much slower than
the iframe method: native async HTML parser →
Inline SVG performance isn't quite there yet
<img src="foo.svg">
<object>, <iframe>, or <embed>
inline is promising, but not where we need it yet → →
Effectively the same thing in most browsers
iframe, object, and embed are roughly equivalent when it comes to embedding SVG
Firefox seems happier with object tags, but otherwise we just use iframe →
<iframe type="text/svg+xml" src="page-1.svg"></iframe> <object type="text/svg+xml" data="page-1.svg"></object>
embedding with iframe or object tags directly doesn't solve our problems, although performance is great. →
still a spinner every time you load a page →
can't modify content before it's loaded
object requests the svg file directly →
Embed object as a tiny SVG that contains a bit of JS
// proxy-svg.js function proxyScript() { /* proxy JS code */ } var SVG = '<svg version="1.1" xmlns="http://www.w3.org/2000/svg">' + '<script><![CDATA[(' + proxyScript.toString() + ')()]]></script>' + '</svg>'; var object = document.createElement('object'); object.type = 'image/svg+xml'; object.data = 'data:image/svg+xml;base64,' + window.btoa(SVG);
first attempt at an iframe/object-based embed strategy that solves spinner and modify SVG content before it's loaded
most complicated solution first, but interesting
downloading SVG text via AJAX, modify the content, and pass to a child object that injects the content into itself
here are some illustrations, explain simplified version →
viewer.js in its nice little Chrome tab
assume already downloaded SVG content, and modified it as necessary, ready to embed the SVG →
// viewer.js var svgElement = document.createElement('object'); svgElement.type = 'image/svg+xml'; svgElement.data = 'data:image/svg+xml;base64,' + btoa(proxySVG); pageElement.appendChild(svgElement);
The parent window embeds a child object via base64-encoded data:url of the proxy-svg script
// viewer.js window.addEventListener('message', handleProxyReadyMessage);
// proxy-svg.js window.parent.postMessage('ready', '*'); window.addEventListener('message', handleViewerMessage);
The proxy-svg script runs inside the newly created object, using the postMessage API to send a mesage to the parent window, which alerts the viewer that it has loaded and is ready to accept SVG content
// viewer.js function handleProxyReadyMessage(event) { if (event.data === 'ready') { svgElement.contentWindow.postMessage(svgContent, '*'); } }
The parent window receives the message, and sends a message containing the SVG content back to the object
// proxy-svg.js function handleViewerMessage(event) { if (event.data) { embedSVG(event.data); } }
The object embeds the SVG content directly into its documentElement using DOMParser and importNode
// proxy-svg.js window.parent.postMessage('loaded', '*');
After the SVG content is embedded, proxy-svg sends a message back to the parent window to say that it's finished loading!
Proxy-svg was a very interesting solution →
overly complex →
no internet explorer, no scripts in data:urls
Once we finally needed to support internet explorer, we searched for other options →
var iframe = document.createElement('iframe'); pageElement.appendChild(iframe); iframe.contentDocument.open(); iframe.contentDocument.write(htmlHeader + svgContent); iframe.contentDocument.close();
Our next solution was much simpler. →
create an empty iframe with empty src or about:blank
considered same domain, you can interact directly with the iframe's contentWindow without worrying about the browser's security policy →
call document.write() from the parent window, and write the SVG content directly into the iframe →
works well for the most part in all browser →
but there were some unfortunate bugs in Firefox and Safari. →
Namely, anything in the <defs> tag seems to be ignored by the browser.
Referencing the images directly fixes the problem, but we put our images in the defs tag so they can be reused throughout the page (which is often the case), so the defs tag is very important →
Also, document.write() causes a spinner in Firefox, which, of course, is unacceptable. →
in order to get around the bugs in Firefox and Safari
yet another solution, "direct proxy" →
combines proxy-svg and document.write() strategies
initialize empty object or iframe with a simple script (similar to proxy-svg) →
object is embeded so browser's security policy doesn't interfere with direct frame-to-frame communication →
embedded script loadSVG takes an SVG string and embeds it into the frame's documentElement with DOMParser →
// to be stringified in the data:url and run inside the child frame function proxySVG() { // actually loads the SVG; called from the parent window window.loadSVG = function (svgText) { // parse the SVG text var dp = new window.DOMParser(), svgDoc = dp.parseFromString(svgText, 'image/svg+xml'); // import the SVG node svgDoc = document.importNode(svgDoc.documentElement, true); // append it! document.documentElement.appendChild(svgEl); }; } var proxyScript = '<script><![CDATA[('+proxySVG+')()]]><'+'/script>';
This is what the embedded "proxy" script looks like
gets strigified into a script tag
exposes a loadSVG method that parses and embeds content →
var object = document.createElement('object'); object.type = 'image/svg+xml'; object.data = 'data:image/svg+xml,<svg>'+proxyScript+'</svg>'; object.onload = function () { object.contentWindow.loadSVG(svgContent); };
In firefox, since the browser's security policy considers data:urls the same domain as the parent, we can simply encode the proxy script into a data:url, embed it as an object, then call the method directly
var iframe = document.createElement('iframe'); iframe.contentDocument.open(); iframe.contentDocument.write(proxyScript); iframe.contentDocument.close(); iframe.onload = function () { iframe.contentWindow.loadSVG(svgContent); };
In other browsers, we can use a similar method to document.write() the script into an iframe, and call the method directly on the iframe's contentWindow
The direct proxy method, although slightly more complex (and possibly less performant) than simple document.write(), seems to solve our problems in Safari and Firefox.
Anyways, thanks for sitting through what was basically a long rant about my adventures in SVG. Please feel free to ask me any questions, and I'll do my best to answer them!