Skip to content

Instantly share code, notes, and snippets.

@ebidel
Last active May 1, 2021 15:42
Show Gist options
  • Star 68 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save ebidel/1ba71473d687d0567bd3 to your computer and use it in GitHub Desktop.
Save ebidel/1ba71473d687d0567bd3 to your computer and use it in GitHub Desktop.
Fast Polymer app loading - optimized for first render, progressively enhanced lazy loading
<!DOCTYPE html>
<html>
<head>
<style>
body.loading #splash {
opacity: 1;
}
#splash {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
transition: opacity 300ms cubic-bezier(0,0,0.2,1);
opacity: 0;
will-change: opacity;
z-index: 1;
background: url(...) no-repeat;
background-color: #E53935;
}
</style>
<!-- 1. Async HTML Imports do not block rending. Benefit of keeping it declarative
(instead of dynamically loading it later in JS) is that the parser can go
to town pre-fetching resources, etc. -->
<link rel="import" id="bundle" href="elements.html" async>
</head>
<!-- 2. Don't use <body unresolved>. It's a simple FOUC solution, but hides
the page until imports and Polymer are loaded. Intead, control FOUC manually with
a splash screen. -->
<body class="loading">
<!-- 3. Light weight splash screen is outside of Polymer/imports and styled by
the main page. 1st paint is fast, even on polyfilled browsers. Alternatively,
one could create an "app shell" and style the page's un-upgraded elements
similar to their final upgraded version. -->
<div id="splash"></div>
<!-- Elements wait on the page and are upgraded when elements.html loads. -->
<paper-drawer-panel>
...
</paper-drawer-panel>
<script src="app.js" async></script>
</body>
</html>
// 4. Conditionally load the webcomponents polyfill if needed by the browser.
// This feature detect will need to change over time as browsers implement
// different features.
var webComponentsSupported = ('registerElement' in document
&& 'import' in document.createElement('link')
&& 'content' in document.createElement('template'));
if (!webComponentsSupported) {
var script = document.createElement('script');
script.async = true;
script.src = '/bower_components/webcomponentsjs/webcomponents-lite.min.js';
script.onload = finishLazyLoading;
document.head.appendChild(script);
} else {
finishLazyLoading();
}
function finishLazyLoading() {
// (Optional) Use native Shadow DOM if it's available in the browser.
window.Polymer = window.Polymer || {dom: 'shadow'};
// 6. Fade splash screen, then remove.
var onImportLoaded = function() {
var loadEl = document.getElementById('splash');
loadEl.addEventListener('transitionend', loadEl.remove);
document.body.classList.remove('loading');
// App is visible and ready to load some data!
};
var link = document.querySelector('#bundle');
// 5. Go if the async Import loaded quickly. Otherwise wait for it.
// crbug.com/504944 - readyState never goes to complete until Chrome 46.
// crbug.com/505279 - Resource Timing API is not available until Chrome 46.
if (link.import && link.import.readyState === 'complete') {
onImportLoaded();
} else {
link.addEventListener('load', onImportLoaded);
}
}
@azogheb
Copy link

azogheb commented Dec 8, 2015

Hey Eric. Thanks for this gist. Super useful!

An issue that I have noticed and not sure how to resolve is that although we have window.Polymer = window.Polymer || {dom: 'shadow'}; the html import may complete BEFORE the app.js runs, and therefore we miss the opportunity to configure polymer before it is to late. Any suggestions?

@ebidel
Copy link
Author

ebidel commented Dec 8, 2015

@dman777
Copy link

dman777 commented Dec 13, 2015

Was var onImportLoaded = function() {} to avoid function hoisting?

@ek
Copy link

ek commented Dec 29, 2015

Very useful, thanks!

@arturparkhisenko
Copy link

I have issue with not working/upgraded element tags (easy to reproduce by adding a breakpoint at any line in app.js).
Working if i remove async attribute from import of elements on line 26.
@ebidel Any ideas how to make it working with async attribute?

@L-u-k-e
Copy link

L-u-k-e commented Jan 11, 2016

An FYI to those who maybe ran into the same issue I did. If you're just testing this method out and you don't have anything in elements.html yet, the script may fire the load event before you set up the onImportLoaded listener.

In this case, the splash screen will never go away! So, make sure you've at least got a couple of imports to load in elements.html

@czellweg
Copy link

A way to figure out the transitionend event name in various browser, the following transitionEndEventName function can be used (I believe this is code from modernizr):

// see http://stackoverflow.com/a/9090128/640539
  function transitionEndEventName() {
    var i;
    var el = document.createElement('div');
    var transitions = {
      'transition': 'transitionend',
      'OTransition': 'otransitionend',
      'MozTransition': 'transitionend',
      'WebkitTransition': 'webkitTransitionEnd'
    };

    for (i in transitions) {
      if (transitions.hasOwnProperty(i) && el.style[i] !== undefined) {
        return transitions[i];
      }
    }
  }

I then use it as follows:

  function finishLazyLoading() {
    // 6. Fade splash screen, then remove.
    var onImportLoaded = function() {
      var loadEl = document.getElementById('splash');
      var transitionEndEvt = transitionEndEventName();
      loadEl.addEventListener(transitionEndEvt, loadEl.remove);

      document.body.classList.remove('loading');
      // App is visible and ready to load some data!
    };

    var link = document.querySelector('#bundle');

    // 5. Go if the async Import loaded quickly. Otherwise wait for it.
    // crbug.com/504944 - readyState never goes to complete until Chrome 46.
    // crbug.com/505279 - Resource Timing API is not available until Chrome 46.
    if (link.import && link.import.readyState === 'complete') {
      onImportLoaded();
    } else {
      link.addEventListener('load', onImportLoaded);
    }
  }

Hope this helps someone.

@czellweg
Copy link

@ebidel: I've played around with your original code as I had non-deterministic behaviour with regards to what external libraries, CSS files and/or other imports have been loaded at a certain stage. For example, sometimes jquery was already present, sometimes it was not.

I believe the main issue is with this check and how this relates to the usage of the async attribute. I'll try to explain:

  • if the elements.html contains many files which they themselves include, for example, large javascript libraries, importing all those elements will take a while (let's say 5s)
  • however: link.import.readyState === 'complete' seems to be true even if the referenced scripts and other html imports have not yet been fully loaded, i.e. if you set a breakpoint at https://gist.github.com/ebidel/1ba71473d687d0567bd3#file-app-js-L38 and look in the network monitor in your debugging tool, many external files/CSS/javascripts etc. have not yet been loaded and are in state pending (i.e. they're not yet 200 OK)
  • looking here, your own article and here, it appears that this is the expected behaviour

Given that, I figured an easy way to check (and make sure that all elements have successfully loaded using the .readyState attribute) is to have another indirection of HTML import, i.e.:

  • move all the needed imports in elements.html to a new file, e.g. imports.html
  • import imports.html in elements.html
  • check within app.js whether the single import element in elements.html has readyState==='complete'
    ** this import, not being async is only in state complete when all dependent files have been loaded

index.html

<html lang="">

<head>
  ...
  <!-- will be replaced with elements/elements.vulcanized.html -->
  <link id="bundle" rel="import" href="elements/elements.html" async>
  <!-- endreplace-->
</head>

<body class="loading fullbleed layout vertical">
  ...
  <template is="dom-bind" id="app">
    ...
  </template>

  <!-- build:js scripts/app.js -->
  <script type="text/javascript" src="scripts/app.js"></script>
  <!-- endbuild-->
</body>
</html>

elements.html

<link id='allImports' rel="import" href="imports.html">

imports.html (example)

...
<link rel="import" href="../bower_components/external-deps/dependency-jquery.html">
<link rel="import" href="../bower_components/external-deps/dependency-materialize.html">
<link rel="import" href="../bower_components/external-deps/dependency-hammerjs.html">

<link rel="import" href="../bower_components/iron-iconset-svg/iron-iconset-svg.html">
<link rel="import" href="../bower_components/iron-flex-layout/classes/iron-flex-layout.html">
<link rel="import" href="../bower_components/iron-icons/iron-icons.html">
<link rel="import" href="../bower_components/iron-icons/av-icons.html">
<link rel="import" href="../bower_components/iron-pages/iron-pages.html">
<link rel="import" href="../bower_components/iron-selector/iron-selector.html">
...

app.js (only relevant snippets shown)

  // Called when all components have been loaded.
  // http://www.html5rocks.com/en/tutorials/webcomponents/imports/
  // http://w3c.github.io/webcomponents/spec/imports/#fetching-import 
  // -> "Every import that is not marked as async delays the load event in the Document."
  var onImportLoaded = function() {
    console.log('All imports have been loaded, took ' + (new Date().getTime() - start) + 'ms');
    initRouting();
    installMaterializeCallbacks();
    checkBrowser();
    // remove the loading class so that app is now visible
    document.body.classList.remove('loading');
  };

  // this method will only be called if webcomponents are supported natively by the browser
  function waitUntilElementsFullyParsed() {
    var link = document.querySelector('#bundle');
    var allImportsDoc = link.import.querySelector('#allImports');
    if (!allImportsDoc) {
      console.log('Needed import element not yet in document, waiting 5ms.');
      setTimeout(waitUntilElementsFullyParsed, 5);
    } else {
      console.log('Import document ready, continuing.');
      // 5. Go if the async Import loaded quickly. Otherwise wait for it.
      // crbug.com/504944 - readyState never goes to complete until Chrome 46.
      // crbug.com/505279 - Resource Timing API is not available until Chrome 46.
      if (allImportsDoc.import && allImportsDoc.import.readyState === 'complete') {
        console.log('All components have already been loaded. Continuing initialization.')
        onImportLoaded();
      } else {
        console.log('Not yet all components have been loaded. Waiting until done.');
        allImportsDoc.addEventListener('load', onImportLoaded);
      }
    }
  }

  // See https://github.com/Polymer/polymer/issues/1381
  window.addEventListener('WebComponentsReady', function() {
    // imports are loaded and elements have been registered
    console.log('WebComponentsReady event caught, took: ' + (new Date().getTime() - start) + 'ms');
    onImportLoaded();

    removeLoaderElement();
  });

  // make sure to install the event listener early enough that removes the splash div
  var loadEl = document.getElementById('splash');
  var transitionEndEvt = transitionEndEventName();
  loadEl.addEventListener(transitionEndEvt, function() {
    console.log('transitionend event caught, removing splash screen.');
    removeLoaderElement();
  });

  // 4. Conditionally load the webcomponents polyfill if needed by the browser.
  // This feature detect will need to change over time as browsers implement
  // different features.
  var webComponentsSupported = ('registerElement' in document && 
    'import' in document.createElement('link') &&
    'content' in document.createElement('template'));

  if (!webComponentsSupported) {
    console.log('Your browser does not support web components natively. Loading polyfill.');
    var script = document.createElement('script');
    script.async = true;
    script.src = '/bower_components/webcomponentsjs/webcomponents-lite.min.js';
    document.head.appendChild(script);
  } else {
    console.log('Your browser supports web components natively, no polyfill needed.');
    waitUntilElementsFullyParsed();
  }

  // see http://stackoverflow.com/a/9090128/640539
  function transitionEndEventName() {
    var i;
    var el = document.createElement('div');
    var transitions = {
      'transition': 'transitionend',
      'OTransition': 'otransitionend',
      'MozTransition': 'transitionend',
      'WebkitTransition': 'webkitTransitionEnd'
    };

    for (i in transitions) {
      if (transitions.hasOwnProperty(i) && el.style[i] !== undefined) {
        return transitions[i];
      }
    }
  }

I've tested this code in Chrome, Firefox, Opera, Safari and I haven't had any more issues regarding components not being available. This way, onImportsLoaded is only ever called if really all components have been loaded (and in case of java script libraries, executed).

Am I missing something or does this sound plausible?

@oliver92
Copy link

@czellweg i still got onImportsLoaded called before all elements were loaded, infact it got called right away, the loading stays for just 2 ms...

Edit: The actual elements are imported, but i think after the imports polymer it`s self does more work, to order the elements on the page, and apply more styling to them, which it can be seen as the splash is already removed by then.

@arturparkhisenko
Copy link

arturparkhisenko commented Jan 18, 2016

How about browser support on it? How to avoid this: crbug.com/504944 - readyState never goes to complete until Chrome 46. on Mac Chrome 41 i have empty element (just tag) and its without async!
<link rel="import" id="bundle" href="elements.html"> ?
I need that because i need to debug my code.. and i got that i described in previous comment.
Tested on Mac Chromium 24 - works fine.
Fixed (i found fix there on gaming youtube) by adding this case with interactive on line37:

if ( link.import && (link.import.readyState === 'complete' || link.import.readyState === 'interactive')) {
  onImportLoaded();
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment