Embedding Complex SVGs Into HTML – (AKA Crazy shit we did to make SVG work for us)



Embedding Complex SVGs Into HTML – (AKA Crazy shit we did to make SVG work for us)

0 0


html5devconf2013

Slides for my HTML5 Dev Conf talk, October, 2013

On Github lakenen / html5devconf2013

Embedding Complex SVGs Into HTML

(AKA Crazy shit we did to make SVG work for us)

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→

The Box View API (formerly Crocodoc) is a service for generating portable, web-viewable versions of documents Documents are converted into HTML5 and viewed in the browser using Viewer.js

Preview and View API use Crocodoc

world-class document viewer →

[scrolling] Here's an example of the document viewer we've built →

We combine three web standards to render documents:

rendering strategy is based on HTML5 →

CSS →

and SVG →

Why SVG?

Can't you do everything with HTML + CSS?

HTML5 is great

aside from canvas it's not enough →

Why SVG?

  • Functionality not available in HTML / CSS
  • Standard, portable, and lightweight
  • Incredibly high rendering fidelity

why SVG? →

provides some operations not available

clipping, masking, blending, paths →

web standards, just work, portable

text file* compresses, important for mobile →

infinite zooming →

Strokes and Fills

in addition to images and text

draw paths, stokes and fills →

Clipping, Masking, and Blending

complex graphics operations

clipping, masking, blending transparency groups →

Rendering Quality and Zooming

Vector graphics scale infinitely (excluding rasterized images)

zooming

"scalable vector graphics"

rasterized images

crisp zooming →

Why not canvas?

  • Significantly more complex to render
  • Requires JavaScript (less portable)
  • Zooming and resizing requires full JS redraw
  • Serious stability issues on mobile

So, what about canvas? →

interesting option, more complex to render →

requires JS →

zooming or resizing requires a redraw in JS

interesting projects, PDF.js →

Several ways to embed SVG content

  • <img src="foo.svg"> and CSS background-image
  • inline SVG (true inline vs DOMParser)
  • <object>, <iframe>, or <embed>

SVG yielded interesting problems, embedding in HTML

several ways to embed →

  • basic img tag or css background-image →

  • inline svg

    • "true" inline vs DOMParser →
  • object, iframe, and embed tags →

Embedding issues

  • Performance and stability (#perfmatters!)
  • Externally linked assets
  • Browser loading indicator

different implications →

  • performance and stability

    • jank-free scrolling and zooming
    • mobile stability →
  • ability to load external assets or modify SVG content on the fly

    • I'll explain why this is necessary →
  • native browser loading indicator spinning on every emebd →

Performance and Stability

  • Reducing SVG complexity
  • Lazyloading + scrolling and zooming performance
  • Memory issues and mobile browsers (*cough* iOS)

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 →

External Assets

  • Converted assets reference common fonts, styles
  • Possible to base64-encode, but bad for performance
  • Modify content on the fly

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 →

External Assets

<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>

The Dreaded Spinner

<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 →

Embed Methods

  • <img src="foo.svg"> and CSS background-image

  • inline SVG (true inline vs DOMParser)
  • <object>, <iframe>, or <embed>

So let's talk about the different embed methods! →

First, let's look at the img tag →

The humble <img> tag

http://www.schepers.cc/svg/blendups/embedding.html

SVG is an image format

image tag supports SVG files

even CSS background-image supports SVG →

<img> tag: no external assets

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) →

<img> tag: no external assets

Solution: base64-encode all assets into nested data: urls

  • Very complex
  • Memory issues and crashing on mobile devices

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 →

Embed methods

<img> is difficult at best – let's look at our other options:

  • <img src="foo.svg">

  • inline SVG
  • <object>, <iframe>, or <embed>

img won't work... other options? →

Inline SVG

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+ →

DOMParser !== inline SVG*

  • Inline SVG (HTML, SVG text parsed on page load) is very fast
  • Document viewer - pages are loaded dynamically
  • SVG embedded with JS is not parsed asynchronously (yet**)
*at least not in Chrome/Blink, and likely not in any browsers currently**http://crbug.com/308321 and http://crbug.com/308768

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 →

Inline (DOMParser)

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

Iframe

This is the same document, but here the SVGs are embedded as iframes.

[scrolling] As you can see, performance is much better

Iframe VS Inline (DOMParser)

Reload iframe Reload inline Reload both

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 →

Embed methods

Inline SVG performance isn't quite there yet

  • <img src="foo.svg">

  • inline SVG
  • <object>, <iframe>, or <embed>

inline is promising, but not where we need it yet → →

Iframe vs Object vs Embed

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 →

Basic embed via iframe/object

  • Spinner :(
  • Can't modify text before loading
<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 →

Proxy-SVG

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 →

Proxy-SVG

viewer.js in its nice little Chrome tab

assume already downloaded SVG content, and modified it as necessary, ready to embed the SVG →

Proxy-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

Proxy-SVG

// 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

Proxy-SVG

// 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

// 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

// 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

  • Too complicated
  • Doesn't work in IE (no scripts in data:urls)

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 →

document.write()

  • Create an empty <iframe src="">
  • document.write() writes SVG directly into iframe

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 →

document.write()

  • Works very well in Chrome and IE
  • Works in FF/Safari, minus <defs> bug
  • Causes spinner in FF

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. →

"Direct Proxy"

  • Combination of Proxy-SVG and document.write() methods
  • Safari - create an iframe, write a script with document.write()
  • Firefox - create an object with data:url encoded script
  • Call script directly from parent window (viewer.js)

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 →

"Direct Proxy"

// 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 →

"Direct Proxy" - Firefox

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

"Direct Proxy" - Safari

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

"Direct Proxy"

  • Solves the <defs> bug in FF/Safari
  • No spinner in Firefox!

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.

Questions?

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!