mikeric / rivets

Lightweight and powerful data binding.
http://rivetsjs.com
MIT License
3.23k stars 310 forks source link

Tips on heavy rendering #495

Open micahlisonbee opened 9 years ago

micahlisonbee commented 9 years ago

I'm rendering an array of of about 1000 objects. The html bindings are very heavy (see below). It's taking about 5 seconds to rivets.bind().

Any suggestions on improving performance? I don't think I can afford to bind in chunks as I'm using a pagination/sorting library in conjuction that needs the entire array in order to sort/paginate.

Here is my HTML for each object (tracks):

<div rv-each-track="tracks" class="track-row row has-hover" rv-download-url="track.direct_path.download_path" rv-api-key="track.track.apikey" rv-media-url="track.direct_path.audio" rv-track-title="track.track.name" rv-wave-data="track.direct_path.wave_default" rv-wave-progress-data="track.direct_path.wave_progress">
<div class="mobile-margin">
    <div class="track-hover desktop-only">
        <div class="hover-play icon-play inline-play"></div>
        <div class="hover-title"><a href="#" class="track-link">{track.track.name}</a><span rv-class="track.track.staff_pick | staffPickClass" data-toggle="tooltip" data-original-title="Staff Pick"></span></div>
        <div class="hover-links">
            <div class="item hamburger holds-tooltip main-hover-item icon-hamburger" data-toggle="tooltip" data-original-title="Alternate Versions"></div>
            <div class="item share main-hover-item popover-button icon-share" data-target="#not-ready-popover"><div class="tooltip-holder holds-tooltip" data-toggle="tooltip" data-original-title="Share Track"></div></div>
            <div class="item playlist icon-playlist-add popover-button holds-tooltip main-hover-item" data-target="#playlist-popover" data-toggle="tooltip" data-original-title="Add to Playlist"></div>
            <div class="item download icon-download holds-tooltip main-hover-item" data-toggle="tooltip" data-original-title="Download Track"></div>
            <div class="item cart last icon-cart-plus holds-tooltip main-hover-item popover-button" data-target="#not-ready-popover" data-toggle="tooltip" data-original-title="Add to Cart"></div>
            <div class="item remove last icon-x holds-tooltip main-hover-item" data-toggle="tooltip" data-original-title="Remove Track" rv-data-delete-track-id="track.track.apikey"></div>
            <div class="clearfix"></div>
        </div>
        <div class="clearfix"></div>
        <div class="track-variations">
            <div rv-each-variation="track.variations.tracks" class="row variation-row" rv-data-track-id="track.track.apikey" rv-api-key="variation.track.apikey" rv-media-url="variation.direct_path.audio" rv-track-title="variation.track.name" rv-wave-data="variation.direct_path.wave_default" rv-wave-progress-data="variation.direct_path.wave_progress">
                <div class="col-md-8 variation-title">{variation.track.name}</div>
                <div class="col-md-2 variation-length">{variation.track.tracklength}</div>
                <div class="track-hover variation">
                    <div class="hover-play icon-play inline-play"></div>
                    <div class="hover-title"><a class="track-link">{variation.track.name}</a></div>
                    <div class="hover-links">
                        <div class="item share popover-button icon-share" data-target="#not-ready-popover"><div class="tooltip-holder holds-tooltip" data-toggle="tooltip" data-original-title="Share Track"></div></div>
                        <div class="item playlist popover-button icon-playlist-add" data-target="#playlist-popover" data-toggle="tooltip" data-original-title="Add to Playlist" data-placement="left"></div>
                        <div class="item download icon-download" rv-data-media-url="track.direct_path.download_path" data-toggle="tooltip" data-original-title="Download Track" data-placement="left"></div>
                        <div class="item cart last icon-cart-plus holds-tooltip popover-button" data-target="#not-ready-popover" data-toggle="tooltip" data-original-title="Add to Cart"></div>
                        <div class="item last remove main-hover-item icon-x"></div>
                        <div class="clearfix"></div>
                    </div>
                    <div class="clearfix"></div>
                </div>
            </div>
            <div class="no-variations" rv-hide="track.variations.tracks | shouldHideNoVariations">There are no alternate versions of this track.</div>
        </div>
    </div>
    <div class="col-md-4 first-title desktop-only"><a class="offset-left track-title-link track-link" href="">{track.track.name}</a><span rv-class="track.track.staff_pick | staffPickClass" data-toggle="tooltip" data-original-title="Staff Pick"></span></span></div>
    <div class="col-md-3 genre desktop-only"><span class="offset-left">{track.genre}</span></div>
    <div class="col-md-2 mood desktop-only"><span class="offset-left">{track.mood}</span></div>
    <div class="col-md-2 canvas desktop-only"><div class="mini-wave offset-left" rv-style-background-image="track.direct_path.wave_canvas"></div></div>
    <div class="col-md-1 last-title duration desktop-only"><span>{track.track.tracklength}</span></div>
    <div class="col-md-1 last-title last-played pull-right desktop-only">{track.lastPlayed}</div>

    <div class="mobile-play icon-play pull-left mobile-only inline-play"></div>
    <div class="mobile-track-title mobile-only track-row-item-margin"><div class="track-title-link">{track.track.name}</div><span rv-class="track.track.staff_pick | staffPickClass"></span></div>
    <div class="mobile-track-buttons pull-right">
        <div class="pull-right mobile-only mobile-button mobile-track-menu-button left-margin icon-plus"></div>
        <div class="pull-right mobile-only mobile-button mobile-variations-button icon-hamburger"></div>
    </div>

    <div class="is-staff-pick hidden">{track.track.staff_pick | staffPickValue}</div>
    <div class="instrument hidden">{track.instrument}</div>
    <div class="industry hidden">{track.industry}</div>
    <div class="tempo hidden">{track.tempo}</div>
    <div class="aggregated-terms hidden">{track.tag_list} {track.track.name}</div>
    <div class="date-last-played hidden">{track.dateLastPlayed}</div>
    <div class="clearfix"></div>
</div>
<div class="track-variations mobile-only">
    <div rv-each-variation="track.variations.tracks" class="row variation-row" rv-api-key="track.track.apikey" rv-media-url="variation.direct_path.audio" rv-track-title="variation.track.name">
        <div class="mobile-margin">
            <div class="col-md-8 variation-title desktop-only">{variation.track.name}</div>
            <div class="col-md-2 variation-length desktop-only">{variation.track.tracklength}</div>
            <div class="track-hover variation">
                <div class="hover-play icon-play inline-play"></div>
                <div class="hover-title"><a href="" class="track-link">{variation.track.name}</a></div>
                <div class="hover-links">
                    <div class="item share popover-button icon-share" data-target="#not-ready-popover"><div class="tooltip-holder holds-tooltip" data-toggle="tooltip" data-original-title="Share Track"></div></div>
                    <div class="item playlist popover-button icon-playlist-add" data-target="#playlist-popover" data-toggle="tooltip" data-original-title="Add to Playlist" data-placement="left"></div>
                    <div class="item download icon-download" data-toggle="tooltip" data-original-title="Download Track" data-placement="left"></div>
                    <div class="item cart last icon-cart-plus holds-tooltip popover-button" data-target="#not-ready-popover" data-toggle="tooltip" data-original-title="Add to Cart"></div>
                    <div class="item last remove main-hover-item icon-x"></div>
                    <div class="clearfix"></div>
                </div>
                <div class="clearfix"></div>
            </div>

            <div class="mobile-play icon-play pull-left mobile-only inline-play"></div>
            <div class="mobile-track-title mobile-only track-row-item-margin"><div class="track-title-link">{variation.track.name}</div></div>
            <div class="mobile-track-buttons pull-right">
                <div class="pull-right mobile-only mobile-button mobile-track-menu-button left-margin icon-plus"></div>
            </div>

        </div>
    </div>
    <div class="no-variations" rv-hide="track.variations.tracks | shouldHideNoVariations">There are no alternate versions of this track.</div>
</div>

benadamstyles commented 9 years ago

So you're not showing 1000 objects on one page? If not, I would probably try to handle the pagination stuff outside rivets, and then bind in chunks as you said. The whole thing about Rivets is that it is intimately entwined with the DOM, which we all know is one of the biggest bottlenecks on the web. So anything Rivets does will hit this bottleneck.

Duder-onomy commented 9 years ago

you can also bind while the html is still in memory in a document fragment then append when all the work is done.

something like.

function renderView(selectorForThisView, viewModel, viewController) {
    var $viewClone = document.querySelector(selectorForThisView).cloneNode(true);

    // toggle some loading state

    rivets.bind($viewClone, { model : viewModel, controller : viewController });

    document.querySelector(selectorForThisView).innerHTML = $viewClone;

    // remove the loading state
}

We do this with Backbone in all of our apps, it makes the initial bind feel almost instant.

That should help you allot, but as @Leeds-eBooks says, you will always have issues when you have that much stuff going on.

micahlisonbee commented 9 years ago

@Duder-onomy thanks for the suggestion but my implementation of your example is about the same speed as a regular bind. Am I doing anything wrong?


renderView('#track-grid', {tracks: tracksPage.allTracks});

function renderView(selectorForThisView) {
    var $viewClone = document.querySelector(selectorForThisView).cloneNode(true);

    rivets.bind($viewClone, {tracks: tracksPage.allTracks});

    document.querySelector(selectorForThisView).innerHTML = $viewClone.innerHTML;

}
micahlisonbee commented 9 years ago

@Leeds-eBooks no we are not showing 1000 objects on the page. We are pulling all 1000 objects into the DOM then a jQuery pagination tool paginates them. Yes handling pagination outside of the DOM (server side) would probably be best but its not possible currently.

woozyking commented 8 years ago

I find that use rv-text="variation.track.name" instead of {variation.track.name} helps a lot too.

Duder-onomy commented 8 years ago

@woozyking Good idea.

Also, I was thinking about this more.

If there are a TON of bindings inside of a massive iterable, then performance will always be an issue. That being said, we can always manage how this performance appears to the user. If the user can at least know something is going on, then that might be enough to satisfy them. This might be a way to achieve this.

* NOT SURE IF THIS WILL WORK * Rivets binders will fire in the order they are found in the DOM. There is a 'priority' attribute that can be used on binders. The higher the priority of the more likely it will be to fire first. That being said, what if we used a super low, possibly negative value on the binder priority, essentially forcing it to fire last. Then we would know when the final binding is complete. This will let us show some sort of loading state while the view is binding.

The binder might look like,

rivets.binders['finished-binding'] = {
    priority : -1000 // force it to fire last
    bind : function() {
        // toggle some model property or fire some event.
    },
    routine : function() {},
    unbind : function() {}
}

the use might look like

<div rv-each-thing='model.things'  a bunch of other binders rv-finished-binding></div>

Just a thought. It might work. I will repost here with a fiddle when I have time to make one. Around lunch time probably.

micahlisonbee commented 8 years ago

@woozyking thanks for the suggestion, though I didn't see any improvements by changing to rv-text="...". I had about 15 instances that I changed in my template...

woozyking commented 8 years ago

@micahlisonbee that's bad to hear. I always find that the use of template tags makes the rendering seems slower, sometimes very noticeable that you can see those { value } every where on the page before they get populated.

Without solving the fundamental performance issue of iterating over a large array of data, I hope there will be some form of event that can fire on rendering and rendered, so that we can at least add some UI components to indicate that the page is not fully loaded (like a toast or spinner).

blikblum commented 8 years ago

@micahlisonbee i'm starting to look into rivets performance and i'd like to use your example to find possible bottlenecks

Can you post the json schema of your data? You can get it by posting a sample of actual data at http://jsonschema.net/

Also if layout has changed since the original post, please resend the HTML binding

micahlisonbee commented 8 years ago

@blikblum please note, this schema is overly bloated and confusing and was going to be re-written.

However, I've since changed my architecture to generate the HTML on the server, it was a huge performance improvement.

Here's the schema that should match the original HTML above (more or less):

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "id": "http://jsonschema.net",
  "type": "object",
  "properties": {
    "mood": {
      "id": "http://jsonschema.net/mood",
      "type": "string"
    },
    "direct_path": {
      "id": "http://jsonschema.net/direct_path",
      "type": "object",
      "properties": {
        "wave_canvas": {
          "id": "http://jsonschema.net/direct_path/wave_canvas",
          "type": "string"
        },
        "wave_default": {
          "id": "http://jsonschema.net/direct_path/wave_default",
          "type": "string"
        },
        "wave_progress": {
          "id": "http://jsonschema.net/direct_path/wave_progress",
          "type": "string"
        },
        "download_path": {
          "id": "http://jsonschema.net/direct_path/download_path",
          "type": "string"
        },
        "audio": {
          "id": "http://jsonschema.net/direct_path/audio",
          "type": "string"
        },
        "watermarked": {
          "id": "http://jsonschema.net/direct_path/watermarked",
          "type": "string"
        }
      }
    },
    "variations": {
      "id": "http://jsonschema.net/variations",
      "type": "object",
      "properties": {
        "count": {
          "id": "http://jsonschema.net/variations/count",
          "type": "integer"
        },
        "tracks": {
          "id": "http://jsonschema.net/variations/tracks",
          "type": "array",
          "items": []
        }
      }
    },
    "tag_list": {
      "id": "http://jsonschema.net/tag_list",
      "type": "string"
    },
    "genre": {
      "id": "http://jsonschema.net/genre",
      "type": "string"
    },
    "tempo": {
      "id": "http://jsonschema.net/tempo",
      "type": "string"
    },
    "instrument": {
      "id": "http://jsonschema.net/instrument",
      "type": "string"
    },
    "industry": {
      "id": "http://jsonschema.net/industry",
      "type": "string"
    },
    "track": {
      "id": "http://jsonschema.net/track",
      "type": "object",
      "properties": {
        "stems": {
          "id": "http://jsonschema.net/track/stems",
          "type": "string"
        },
        "tracklength": {
          "id": "http://jsonschema.net/track/tracklength",
          "type": "string"
        },
        "inactive": {
          "id": "http://jsonschema.net/track/inactive",
          "type": "boolean"
        },
        "apikey": {
          "id": "http://jsonschema.net/track/apikey",
          "type": "string"
        },
        "audio_content_type": {
          "id": "http://jsonschema.net/track/audio_content_type",
          "type": "string"
        },
        "geokey": {
          "id": "http://jsonschema.net/track/geokey",
          "type": "null"
        },
        "name": {
          "id": "http://jsonschema.net/track/name",
          "type": "string"
        },
        "staff_pick": {
          "id": "http://jsonschema.net/track/staff_pick",
          "type": "null"
        },
        "audio_file_size": {
          "id": "http://jsonschema.net/track/audio_file_size",
          "type": "integer"
        },
        "audio_file_name": {
          "id": "http://jsonschema.net/track/audio_file_name",
          "type": "string"
        },
        "lyrics": {
          "id": "http://jsonschema.net/track/lyrics",
          "type": "string"
        },
        "bpm": {
          "id": "http://jsonschema.net/track/bpm",
          "type": "string"
        }
      }
    }
  },
  "required": [
    "mood",
    "direct_path",
    "variations",
    "tag_list",
    "genre",
    "tempo",
    "instrument",
    "industry",
    "track"
  ]
}
blikblum commented 8 years ago

Thanks. It will be used just as a benchmark test. I will post the perf tests here as are done.

blikblum commented 8 years ago

I have to adapt the schema to get working. Here's the codepen: http://codepen.io/blikblum/pen/eJKOXo/?editors=1010 Here's the jsperf: http://jsperf.com/rivetsjs-heavy-rendering/2

Duder-onomy commented 8 years ago

Hey Everyone in here,

Very excited to say, A negative binder priority actually works!

So this means we can implement an rv-cloak to prevent the FOUC. Here is a gist I made demonstrating with a rv-each with 10,000 items.

rivets.binders.cloak = {
    priority : -1000,
    bind : function(el) {
        el.style.opacity = 1;
    }
}

This important bits about the rv-cloak binder.

[rv-cloak] {
    opacity: 0;
    transition: opacity 0.5s ease;
}