Am I paranoid? "Brutally" big Polymer website after Vulcanize. What can I do to fix it?

469 Views Asked by At

Maybe I'm paranoid. I always like to have my code as slim as possible. I always target my websites to be under 1.5 MB (All images compressed and resized as appropriate(. I started working with Polymer the month before thinking that I could shave off those 150 KBs from Bootstrap and 90 KB from jQuery, and have a relatively lightweight site.

I've just vulcanized my elements.html file and I am horrified. The beast is 947KB without images, just bare HTML and JS. I have around 40 custom elements + Couple of the Elements catalog (and I'm not even close to creating new elements). (GZip is 307.40 KB out of 947KB) (Using ASP.NET MVC5 and .NET 4.6).

With a regular 3G connection, it takes about 5.15 seconds to load in Chrome 52 (which is awful). The Polymer Shop demo loads beautifully fast (<3 seconds from cold cache in regular 3G)

First of all, is this acceptable? I'm trying to hit before the 3 second mark (or get to it as close as possible).

Also, there are many JavaScript files that are being loaded part of Vulcanize which I don't need.

I've seen this Gist: Vulcanize and Polymer auto Lazy Loading but I don't know what to do with it.

These are the imports of my elements.html file:

<link rel="import" href="../bower_components/app-route/app-route.html">
<link rel="import" href="../bower_components/app-route/app-location.html">


<link rel="import" href="../bower_components/app-layout/app-drawer-layout/app-drawer-layout.html">
<link rel="import" href="../bower_components/app-layout/app-drawer/app-drawer.html">

<link rel="import" href="./pgarena-drawer/pgarena-drawer.html">
<link rel="import" href="./pgarena-navbar/pgarena-navbar.html">
<link rel="import" href="./pgarena-auth/pgarena-oauth/pgarena-oauth.html">

<link rel="import" href="./routes/pgarena-app.html">

Then all my custom elements (pgarena) have more polymer components built into it.

I've tried several combinations (Only with my elements) and (Only with the Polymer elements shown) and I've had varied results (as expected)

I don't know what to do... Before resorting to a hacky stuff... Any recommendations?

2

There are 2 best solutions below

0
On BEST ANSWER

Ok, people, bear with me. This is going to be a long answer. It can become a little bit hairy. First of all, this was a Polymer 1.x solution. I don't know what of this has changed for version 2.0

TL;DR: We get the URLs of the .HTML and use JavaScript to create the link attribute (HTML import) to load the element. We check with Polymer using Polymer.isInstance(element) to see if the object has been set or not.

Here's the code:

For this to work, I was using iron-pages, and custom JavaScript.

We have our app, as the following:

Note*: The following code I had it in the same file, you can separate it as you wish.

<!-- Main Entry point for the application. This will work as the main "Controller"-->

<link rel="import" href="../../bower_components/polymer/polymer.html">

<link rel="import" href="../../bower_components/app-route/app-location.html">
<link rel="import" href="../../bower_components/app-route/app-route.html">
<link rel="import" href="../../bower_components/iron-pages/iron-pages.html">
<dom-module id="pgarena-app">
    <template>
        <pgarena-action-config></pgarena-action-config>
        <app-route route="{{route}}"
                   pattern="/:page"
                   data="{{data}}"
                   tail="{{tail}}">
        </app-route>
        <iron-pages selected="[[data.page]]" attr-for-selected="title" fallback-selection="404">
            <pgarena-home-app title="" route="[[tail]]"></pgarena-home-app>
            <pgarena-tournament-app title="tournaments" route="[[tail]]"></pgarena-tournament-app>
            <!--<pgarena-clash-app title="clash" route="[[tail]]"></pgarena-clash-app>-->
            <pgarena-account-app title="account" route="[[tail]]"><content></content></pgarena-account-app>
            <pgarena-teams-app title="teams" route="[[tail]]"></pgarena-teams-app>
            <div title="404">
                <h1>{{data.page}} could not be found!</h1>
            </div>
        </iron-pages>

    </template>
    <script>
        (function () {
            'use strict';
            Polymer({
                is: 'pgarena-app',
                ready: function () {
                    /* console.log("Route is ");
                     console.log(this.data.page);
                    console.log(this.tail);*/
                    document.addEventListener('iron-select',
                        function (event) {
                            /*
                            console.log("---------------------");
                            console.log(event);
                            console.log("---------------------");*/
                            var element = getSelectedElement(event);
                            var tagName = element.tagName.toLowerCase();
                            LazyLoad(Polymer, element, tagName, event.target);
                        });
                }
            });
        })();

    </script>

Let's get some things first:

  1. My app is called: "pgarena-app"
  2. I don't know if this has been fixed, but the app-route element has a problem with two-way data binding. Meaning that for iron-pages, I had to use the double brackets [[]] to one-way data bind.
  3. App route passes the information from the url to iron-pages so it can toggle the different elements.
  4. This is not mandatory, and I don't know if this is the right way to do it. I divided my application into "views", which are elements by itself. They load all the elements required to that "view" by. Note: This has no effect, whatsoever in lazy loading.

Note, that the elements are not included in the URL, because we're going to lazy load them.

Let's go to the JavaScript portion of this element:

<script>
    (function () {
        'use strict';
        Polymer({
            is: 'pgarena-app',
            ready: function () {

                document.addEventListener('iron-select',
                    function (event) {

                        var element = getSelectedElement(event);
                        var tagName = element.tagName.toLowerCase();
                        LazyLoad(Polymer, element, tagName, event.target);
                    });
            }
        });
    })();

</script>

The code is simple in here. We define our element, and we listen to the iron select event. This signals us that iron-page has been selected. We lazy load the element if it's not there. The magic behind this, is in the custom LazyLoad JavaScript, which is below.

<script>
        /**
        * Defines all the routes of the imports in here
        *
        * This is how it goes: The Key name is the tag name of the element.
        * The value is the relative URL from the elements folder.
        *
        * You then get the element's tag name and look for it.
        *
        * DO NOT PUT TRAILING SLASH BEFORE THE URL! Thanks :)
        **/
        var PGArena = PGArena || {};
        PGArena.LazyLoad =
        {
            "pgarena-home-app": "routes/home/pgarena-home-app.html",
            "pgarena-tournament-app": "routes/tournament/pgarena-tournament-app.html",
            "pgarena-account-app": "routes/account/pgarena-account-app.html",
            "pgarena-clash-app": "routes/clash/pgarena-clash-app.html",
            "pgarena-teams-app": "routes/teams/pgarena-teams-app.html",

            "pgarena-tournament-index-view": "views/tournament/pgarena-tournament-index-view/pgarena-tournament-index-view.html",
            "pgarena-tournament-list-view": "views/tournament/pgarena-tournament-list-view/pgarena-tournament-list-view.html",

            "pgarena-account-index-view": "views/account/pgarena-account-index-view/pgarena-account-index-view.html",
            "pgarena-account-login-view": "views/account/pgarena-account-login-view/pgarena-account-login-view.html",
            "pgarena-account-register-view": "views/account/pgarena-account-register-view/pgarena-account-register-view.html",
            "pgarena-account-confirm-email-view": "views/account/pgarena-account-confirm-email-view/pgarena-account-confirm-email-view.html",
            "pgarena-account-oauth-view": "views/account/pgarena-account-oauth-view/pgarena-account-oauth-view.html",

            "pgarena-clash-index-view": "views/clash/pgarena-clash-index-view/pgarena-clash-index-view.html",
            "pgarena-clash-brawl-view": "views/clash/pgarena-clash-brawl-view/pgarena-clash-brawl-view.html",

            "pgarena-teams-index-view": "views/team/pgarena-teams-index-view/pgarena-teams-index-view.html",
            "pgarena-teams-create-view": "views/team/pgarena-teams-create-view/pgarena-teams-create-view.html"

        };
        /**
        * This variable keeps track of all the vulcanized elements.
        *
        **/
        PGArena.Vulcanized = {

        }
        /**
        * Global Placeholder for checking which is the selected item of the iron-selectors that
         are ready for lazy loading.
        **/
        PGArena.IronSelected = {

        }

        /**
        * LazyLoad
        *
        * Lazy Loads the elements as needed. This function is triggered by iron-select
        * event. If the element is already registered, then it is not loaded again.
        *
        * Polymer => Dependency Injection of the Polymer object. (Polymer itself)
        * element => The element (DOM-wise: a.k.a tags with everything)
        * elementName => The element's name.
        * selectorTrigger => The element who triggered the select.
        **/
        function LazyLoad(Polymer, element, elementName, selectorTrigger) {
            if (Polymer.isInstance(element)) {
                // console.log(elementName + " is already registered ;)");
                return;
            } else {
                //console.log(elementName+" isn't registered. On its way for Lazy Loading!");
            }
            //console.log("Lazy Load Started");
            var hasProp = PGArena.LazyLoad.hasOwnProperty(elementName);
            if (!hasProp) {
                console.log("Property " + elementName + " not found for Lazy Loading");
                return;
            }

            var href = PGArena.LazyLoad[elementName];
            LoadImportAsync(href, elementName, selectorTrigger); 
        }

        function Spinner(elementName, active) {
            var paperId = 'js-' + elementName;
            var queryName = active ? elementName : paperId;
            var createElem = active ? 'paper-spinner-lite' : elementName;
            var elem = document.querySelector(queryName);
            var spinner = document.createElement(createElem);
                spinner.setAttribute('active', '');
            if (elem === null || elem === undefined)
                return;
            console.log("Element Name is");
            console.log(queryName);
            console.log("Element is");
            console.log(elem);

            console.log("Spinner is:");
            console.log(spinner);
            if (active) {
                spinner.setAttribute('id', 'js-' + elementName);

                console.log("replacing time");
                elem.parentNode.replaceChild(document.createTextNode("Caca"), elem);
                //elem.parentNode.replaceChild(spinner, elem);
            }
            else {
                console.log("Replaced");
                //elem.parentNode.replaceChild(elem, spinner);
            }

        }

        function ForcedLoad() {

        }
        /**
        * Loads the required import and appends it to the document. It really doesn't
        * matter where it is appended.
        *
        **/
        function LoadImportAsync(href, elementName) {

            var link = document.createElement('link');
            link.rel = 'import';
            link.href = getBaseUrl() + "/NodeJS/Polymer/app/elements/" + href;
            link.setAttribute('async', ''); // make it async!
            link.onload = function () { Spinner(elementName, false); }
            link.onerror = function (e) { console.log("There was an error loading " + elementName + ". Please Check the URL") };
            document.head.appendChild(link);
        }

        function getBaseUrl() {
            var pathArray = location.href.split('/');
            var protocol = pathArray[0];
            var host = pathArray[2];
            return protocol + '//' + host;
        }

        /**
        * On non-blink browsers (a.k.a Firefox , Edge, Internet Explorer)
        * The event.srcElement is undefined. We need to search for it ourselves.
        *
        * The way we do that is that we get the current targetted element which is the iron form.
        * Retrieve its selection mechanism and the supposed element's index.
        *
        * We proceed by query Selecting the element in the DOM all the way until we nab it.
        * Then we are faced with the next challenge. We don't know if the element is using an
        * index-based approach (0, 1, 2...) or an attribute approach(title="home", title="tournament",etc.)
        *
        * So we proceed to fetch its selection mechanism by grabbing the attrForSelected. If null, it means that
        * it is using the index-based approach. We continue and get the children position at the element.
        *
        * Note that selectedAttr variable will return me either the index or the selected attribute's value.
        * So it's going to be 0, 1, 2 if using the index based approach.
        *
        **/

        function getSelectedElement(event) {
            if (event.srcElement !== undefined)
                return event.srcElement.selectedItem;

            var element = event.target;

            //Get the current selected attribute:
            var selectedAttr = element.selected;
            //Gets the attribute that is being used for selection:
            var attrForSelected = element.attrForSelected;

            //This means that it is not using index based
            if (attrForSelected !== null) {
                return element.querySelector('[' + attrForSelected + '="' + selectedAttr + '"]');
            }

            //Continues using index based:
            var childelem = element.children[parseInt(selectedAttr)];
            return childelem;


        }



    </script>

The first thing we do is to define the URLs relative to the document I have. I do this by defining a json with a key whose name is the title attribute of the iron-pages and the value with the relative URL to this document (the pgarena-app).

What I mean, is that in the case I want to load pgarena-tournament-appand my pgarena-app (my main entry point to the application) is in www/polymer/pgarena-app.htmland my pgarena-tournament-app is in www/polymer/routes/tournament/pgarena-tournament-app.html, since this is relative my JSON will be:

var PGArena = PGArena || {}; PGArena.LazyLoad = { "tournament" : "routes/tournament/pgarena-tournament-app.html", };

Note PGArena.LazyLoad can be anything, this is a global variable I defined with the PGArena namespace.

Then, we see that the code LazyLoad is called:

function LazyLoad(Polymer, element, elementName, selectorTrigger) {
        if (Polymer.isInstance(element)) {
            // console.log(elementName + " is already registered ;)");
            return;
        } else {
            //console.log(elementName+" isn't registered. On its way for Lazy Loading!");
        }
        //console.log("Lazy Load Started");
        var hasProp = PGArena.LazyLoad.hasOwnProperty(elementName);
        if (!hasProp) {
            console.log("Property " + elementName + " not found for Lazy Loading");
            return;
        }

        var href = PGArena.LazyLoad[elementName];
        LoadImportAsync(href, elementName, selectorTrigger); 
    }

What I do in here is to check if the Element that I want to lazy load has been referenced in the JSON I defined (PGarena.LazyLoad). If it's not in there, then what I do is that I log that message. If it's there, and it has not loaded, then I Asynchronously load it by creating the HTML import and appending it to the head:

 /**
        * Loads the required import and appends it to the document. It really doesn't
        * matter where it is appended.
        *
        **/
        function LoadImportAsync(href, elementName) {

            var link = document.createElement('link');
            link.rel = 'import';
            link.href = getBaseUrl() + "/NodeJS/Polymer/app/elements/" + href;
            link.setAttribute('async', ''); // make it async!
            link.onload = function () { Spinner(elementName, false); }
            link.onerror = function (e) { console.log("There was an error loading " + elementName + ". Please Check the URL") };
            document.head.appendChild(link);
        }

Please notice (I don't know if they've fixed this). There's a polyfill for HTML imports for Firefox, and Edge, and Safari(I believe). The polyfill uses XHR (AJAX) for loading the imports!!! I mention this because at the beginning I tried to intercept the HTML Imports and in Google Chrome it didn't work.

Let me know if you need anything else. As you can see, I tried to use the spinner, but I didn't get it to work. Hope this helps!

7
On

I'd start with the fact that Vulcanize only merges files together by default. Have you added the switches to remove comments for example?

Also, you would want to minify you bundled HTML and JS files. Most example will show you a Gulp setup but you could just as well minify the vulcanized files in a second step in an npm script.

Maybe this post will help: https://jongeho.wordpress.com/2015/06/24/endeavors-with-polymer-1-0-vulcanize-crisper-html-minifier/

That said, it is true that a Web Component-rich app will naturally be quite large. It is something I've also been noticing