Optimizing Perceived Performance

  1. Testing

  2. Optimizing Perceived Performance

  3. About Me

  4. My Startup

    “DBDB”

    http://dbdb.local/home
  5. The Problem

  6. The Problem (Con’t)

    def bagfactor
      sleep(2)
      rand(50) / 10.0
    end
    
  7. http://github.com/dce/dbdb

    • Rails app
    • public/slides.html
    • public/javacripts/jquery.jquinote.js
  8. It could happen to you

  9. First Impressions

  10. Up to... User response
    0.1 second instantaneous
    1 second responsive
    10 seconds slow
    > 10 seconds gone

    – About Face 3

    http://dbdb.local/
  11. Bring in the AJAX

  12. Progressive Enhancement

  13. Unobtrusive Javascript

  14. HIJAX

  15. jQuery

  16. class DbsController < ApplicationController
      def show
        @db = Db.find(params[:id])
        respond_to do |format|
          format.html
          format.js do
            render :partial => "profile",
              :locals => { :db => @db }
          end
        end
      end
    end
    
  17. $(".db-list a").click(function() {
      link = $(this);
      $.ajax({url: $(this).attr("href"),
        success: function(src) {
          link.parents("dt").after("<dd>" + src + "</dd>");
          link.unbind('click').click(function() {
            $(this).parents("dt").next("dd").toggle();
            return false;
          })
        }
      });
      return false;
    });
    
    http://dbdb.local/
  18. Make It Snappy

  19. $(".db-list a").click(function() {
      link = $(this);
      link.parents("dt").after('<dd class="spinner"></dd>')
        .next("dd").hide().slideDown("slow");
    
      link.unbind("click").click(function() {
        $(this).parents("dt").next("dd").slideToggle();
        return false;
      });
    
      $.ajax({url: link.attr("href"),
        success: function(src) {
          link.parents("dt").next("dd").html(src)
            .removeClass("spinner");
        }
      });
      return false;
    });
    
    http://dbdb.local/
  20. Take Advantage of Downtime

  21. jQuery.fn.loadContentInOrder = function() {
      return this.each(function() {
        link = $(this);
    
        $.ajax({url: link.attr("href"),
          success: function(src) {
            display = link.parents("dt").next("dd").html(src)
              .removeClass("spinner")
              .next("dt").find("a").loadContentInOrder();
          }
        });
      });
    };
    
    $(".db-list a").each(function() {
      $(this).parents("dt").after("<dd class=\"spinner\"></dd>")
        .next("dd").hide();
    });
    
    $(".db-list a").click(function() {
      $(this).parents("dt").next("dd").slideToggle();
      return false;
    });
    
    $(".db-list a:first").loadContentInOrder();
    
    http://dbdb.local/
  22. Isolate Bottlenecks

  23. JSON

    { "db": {
        "id": 13,
        "name": "Tyler Hansbrough",
        "occupation": "UNC Basketball Player",
        "bagfactor": 1.3,
        "avatar_id": 61
      }
    }
    
  24. class DbsController < ApplicationController
      def show
        @db = Db.find(params[:id])
        respond_to do |format|
          format.html
          format.js { ... }
          format.json do
            render :json => { :bagfactor => @db.bagfactor }
          end
        end
      end
    end
    
  25. jQuery.fn.loadBagfactor = function() {

      img = $(this);

    
      $.ajax({url: this.parents("dd").prev("dt")
          .find("a").attr("href"),

        data: { format: "json" }, dataType: "json",

        success: function(db) {

          img.replaceWith(db.bagfactor);

          $("img.spinner:first").loadBagfactor();

        }

      });
    };

    
    $("img.spinner:first").loadBagfactor();
    
    http://dbdb.local/v4
  26. Worst Best Solution

    • Most information in least time
    • Poor degredation
    • Optimize for users' needs
  27. New Problem

  28. http://dbdb.local/dbs/new
  29. Remove Blocking Operations

  30. The Usual Way

    • Hidden iFrame
    • Second form w/ iFrame as target
    • Server sends back JS to update page
  31. Something Sorta Nuts

  32. avatars/new.html.erb

    <% form_for @avatar, :html => { :multipart => true } do |f| %>
      <%= f.file_field :image %>
    <% end %>
    
    <% javascript_tag do %>
      $("input").change(function() {
        $(this).hide().after('<%= image_tag "spinner.gif" %>');
        $(this).parents('form').submit();
      });
    <% end %>
    
  33. avatars/create.html.erb

    <%= image_tag @avatar.image.url(:thumb) %>
    
    <% javascript_tag do %>
      $("form", top.document).append(
        '<%= hidden_field_tag "db[avatar_id]", @avatar.id %>');
    <% end %>
    
  34. $("input[type=file]").replaceWith(
      '<iframe src="/avatars/new"></iframe>');
    
    http://dbdb.local/dbs/new
  35. Cool? Lame?

    (It doesn't matter)

  36. Server Side

  37. “At least 80 percent of the time it takes to display a web page happens after the HTML document has been downloaded.”

    — Steve Souders, High Performance Web Sites

  38. Fewer HTTP Requests

    • Combine scripts & CSS files
    • Use CSS sprites
    • Avoid redirects when possible
  39. Optimize Browser Caching

    • Use a far-future expires header
    • Put JS & CSS in external files
    • Apache mod_expires
  40. Reduce File Size

    • Minify JS - asset_packager
    • Use GZIP
    • Apache mod_deflate
  41. What's Next

    Rails 3

  42. The link_to_remote Helper

    <%= link_to_remote @db.name, :url => db_url(@db),
           :method => :get %>
    
  43. The Old Way

    # <%= link_to_remote @db.name, :url => db_url(@db),
    #       :method => :get %>
    
    <a href="#" onclick="$.ajax({data:'authenticity_token=' +
      encodeURIComponent('2b79b50423e099...'),
      dataType:'script', type:'get',
      url:'http://dbdb.local/dbs/13'}); return false;">
        Tyler Hansbrough
    </a>
    
  44. The New Way

    # <%= link_to_remote @db.name, :url => db_url(@db),
    #       :method => :get %>
    
    <a href="/dbs/13" data-remote="true" data-method="get">
      Tyler Hansbrough
    </a>
    
  45. The New Way (Con’t)

    # <%= link_to_remote @db.name, :url => db_url(@db),
    #       :method => :get %>
    
    # <a href="/dbs/13" data-remote="true" data-method="get">
    #   Tyler Hansbrough
    # </a>
    
    $("a[data-remote=true]").click(function() {
      $.ajax({url: $(this).attr("href"),
        method = this.dataset.method,
        success: function(src) {
          link.parents("dt").after("<dd>" + src + "</dd>");
        }
      });
    });
    
  46.  <a href="/dbs/13" class="remote">
       Tyler Hansbrough
     </a>
    
     <!--
     $("a.remote").click(function() {
        $.ajax({url: $(this).attr("href"),
        ...
        });
     });
     -->
     
  47. Conclusion

  48. Consider perceived performance

  49. Write your own JS

  50. Optimize your server config

  51. Thank You

    http://speakerrate.com/talks/1182

  52. Your Turn

    Stories? Questions?