$30 off During Our Annual Pro Sale. View Details »

ConnectJS Dynamically Sassy

ConnectJS Dynamically Sassy

Jeremy Fairbank

October 16, 2015
Tweet

More Decks by Jeremy Fairbank

Other Decks in Programming

Transcript

  1. Dynamically Sassy
    Generating Dynamic CSS in Rails
    Jeremy Fairbank
    @elpapapollo
    jeremyfairbank.com

    View Slide

  2. Hi, I’m Jeremy
    jfairbank @elpapapollo blog.jeremyfairbank.com

    View Slide

  3. We help brands excel.
    simplybuilt.com
    Your website, SimplyBuilt.
    pushagency.io

    View Slide

  4. View Slide

  5. How to allow site theme
    color customization?

    View Slide

  6. Challenges
    • Generate stylesheet from user input.
    • Avoid duplication between dynamic
    and static stylesheets.
    • Performance.

    View Slide

  7. View Slide

  8. GET /palettes/custom.css?
    color=steelblue

    View Slide

  9. GET /palettes/custom.css?
    color=steelblue

    View Slide

  10. CSS

    View Slide

  11. CSS

    View Slide

  12. Roadmap
    • Why Sass?
    • Sass and Ruby interoperability.
    • Sass engine rendering.
    • Web server performance.
    • Caching and background processing.
    • Refactoring.

    View Slide

  13. Why Sass ?
    • Modularity
    • DRY
    • Loops, functions, lists, and maps.

    View Slide

  14. $palettes: (
    (bg: red, text: pink)
    (bg: blue, text: cyan)
    (bg: green, text: lime)
    );
    @for $i from 1 through 3 {
    $palette: nth($palettes, $i);
    body.palette-#{$i} {
    background: map-get($palette, bg);
    color: map-get($palette, text);
    }
    }

    View Slide

  15. $palettes: (
    (bg: red, text: pink)
    (bg: blue, text: cyan)
    (bg: green, text: lime)
    );
    @for $i from 1 through 3 {
    $palette: nth($palettes, $i);
    body.palette-#{$i} {
    background: map-get($palette, bg);
    color: map-get($palette, text);
    }
    }
    List

    View Slide

  16. $palettes: (
    (bg: red, text: pink)
    (bg: blue, text: cyan)
    (bg: green, text: lime)
    );
    @for $i from 1 through 3 {
    $palette: nth($palettes, $i);
    body.palette-#{$i} {
    background: map-get($palette, bg);
    color: map-get($palette, text);
    }
    }
    Map

    View Slide

  17. $palettes: (
    (bg: red, text: pink)
    (bg: blue, text: cyan)
    (bg: green, text: lime)
    );
    @for $i from 1 through 3 {
    $palette: nth($palettes, $i);
    body.palette-#{$i} {
    background: map-get($palette, bg);
    color: map-get($palette, text);
    }
    }
    Loop

    View Slide

  18. $palettes: (
    (bg: red, text: pink)
    (bg: blue, text: cyan)
    (bg: green, text: lime)
    );
    @for $i from 1 through 3 {
    $palette: nth($palettes, $i);
    body.palette-#{$i} {
    background: map-get($palette, bg);
    color: map-get($palette, text);
    }
    }
    Function

    View Slide

  19. body.palette-1 {
    background: red;
    color: pink;
    }
    body.palette-2 {
    background: blue;
    color: cyan;
    }
    body.palette-3 {
    background: green;
    color: lime;
    }
    $ sass -t expanded example.scss > example.css

    View Slide

  20. Reusability?
    $palettes: (
    (bg: red, text: pink)
    (bg: blue, text: cyan)
    (bg: green, text: lime)
    );
    @for $i from 1 through 3 {
    $palette: nth($palettes, $i);
    body.palette-#{$i} {
    background: map-get($palette, bg);
    color: map-get($palette, text);
    }
    }

    View Slide

  21. // theme1.scss
    $palettes: (
    (bg: red, text: pink)
    (bg: blue, text: cyan)
    (bg: green, text: lime)
    );
    @import 'ui';
    // theme2.scss
    $palettes: (
    (bg: beige, text: gold)
    (bg: mauve, text: purple)
    (bg: tan, text: brown)
    );
    @import 'ui';
    // _ui.scss
    @for $i from 1 through 3 {
    $palette: nth($palettes, $i);
    body.palette-#{$i} {
    background: map-get($palette, bg);
    color: map-get($palette, text);
    }
    }

    View Slide

  22. Static Data
    // theme1.scss
    $palettes: (
    (bg: red, text: pink)
    (bg: blue, text: cyan)
    (bg: green, text: lime)
    );
    @import 'ui';
    // theme2.scss
    $palettes: (
    (bg: beige, text: gold)
    (bg: mauve, text: purple)
    (bg: tan, text: brown)
    );
    @import 'ui';

    View Slide

  23. Dynamic Data?
    // theme1_dynamic.scss
    $palettes: get-dynamic-palettes();
    @import 'ui';

    View Slide

  24. # config/initializers/sass.rb
    module Sass::Script::Functions
    # Generate palettes of random hex values
    def get_dynamic_palettes
    palettes = 3.times.map do
    Sass::Script::Value::Map.new({
    Sass::Script::Value::String.new('bg') => random_hex,
    Sass::Script::Value::String.new('text') => random_hex
    })
    end
    Sass::Script::Value::List.new(palettes, :space)
    end
    private def random_hex
    Sass::Script::Value::Color.from_hex(
    '#%06x' % (rand * 0xffffff)
    )
    end
    end

    View Slide

  25. # config/initializers/sass.rb
    module Sass::Script::Functions
    # Generate palettes of random hex values
    def get_dynamic_palettes
    palettes = 3.times.map do
    Sass::Script::Value::Map.new({
    Sass::Script::Value::String.new('bg') => random_hex,
    Sass::Script::Value::String.new('text') => random_hex
    })
    end
    Sass::Script::Value::List.new(palettes, :space)
    end
    private def random_hex
    Sass::Script::Value::Color.from_hex(
    '#%06x' % (rand * 0xffffff)
    )
    end
    end

    View Slide

  26. # config/initializers/sass.rb
    module Sass::Script::Functions
    # Generate palettes of random hex values
    def get_dynamic_palettes
    palettes = 3.times.map do
    Sass::Script::Value::Map.new({
    Sass::Script::Value::String.new('bg') => random_hex,
    Sass::Script::Value::String.new('text') => random_hex
    })
    end
    Sass::Script::Value::List.new(palettes, :space)
    end
    private def random_hex
    Sass::Script::Value::Color.from_hex(
    '#%06x' % (rand * 0xffffff)
    )
    end
    end

    View Slide

  27. # config/initializers/sass.rb
    module Sass::Script::Functions
    # Generate palettes of random hex values
    def get_dynamic_palettes
    palettes = 3.times.map do
    Sass::Script::Value::Map.new({
    Sass::Script::Value::String.new('bg') => random_hex,
    Sass::Script::Value::String.new('text') => random_hex
    })
    end
    Sass::Script::Value::List.new(palettes, :space)
    end
    private def random_hex
    Sass::Script::Value::Color.from_hex(
    '#%06x' % (rand * 0xffffff)
    )
    end
    end

    View Slide

  28. Dynamic Data from User?
    // theme1_dynamic_user.scss
    $palettes: get-dynamic-palettes-from-user-magically();
    @import 'ui';

    View Slide

  29. Using User Input
    • Normal assets are precompiled, so can’t
    use asset pipeline for dynamic content.
    • Manually render stylesheet from a Rails
    controller.
    • Utilize Sass::Engine class.

    View Slide

  30. class PalettesController < ApplicationController
    TEMPLATE = <<-SCSS.freeze
    $palettes: get-custom-palettes();
    @import 'ui';
    SCSS
    def custom_palettes
    @css = Sass::Engine.new(TEMPLATE, {
    syntax: :scss,
    style: :expanded,
    load_paths: [
    Rails.root.join('app/assets/stylesheets')
    ],
    custom: { color: params[:custom_color] }
    }).render
    end
    end

    View Slide

  31. class PalettesController < ApplicationController
    TEMPLATE = <<-SCSS.freeze
    $palettes: get-custom-palettes();
    @import 'ui';
    SCSS
    def custom_palettes
    @css = Sass::Engine.new(TEMPLATE, {
    syntax: :scss,
    style: :expanded,
    load_paths: [
    Rails.root.join('app/assets/stylesheets')
    ],
    custom: { color: params[:custom_color] }
    }).render
    end
    end

    View Slide

  32. class PalettesController < ApplicationController
    TEMPLATE = <<-SCSS.freeze
    $palettes: get-custom-palettes();
    @import 'ui';
    SCSS
    def custom_palettes
    @css = Sass::Engine.new(TEMPLATE, {
    syntax: :scss,
    style: :expanded,
    load_paths: [
    Rails.root.join('app/assets/stylesheets')
    ],
    custom: { color: params[:custom_color] }
    }).render
    end
    end

    View Slide

  33. class PalettesController < ApplicationController
    TEMPLATE = <<-SCSS.freeze
    $palettes: get-custom-palettes();
    @import 'ui';
    SCSS
    def custom_palettes
    @css = Sass::Engine.new(TEMPLATE, {
    syntax: :scss,
    style: :expanded,
    load_paths: [
    Rails.root.join('app/assets/stylesheets')
    ],
    custom: { color: params[:custom_color] }
    }).render
    end
    end

    View Slide

  34. class PalettesController < ApplicationController
    TEMPLATE = <<-SCSS.freeze
    $palettes: get-custom-palettes();
    @import 'ui';
    SCSS
    def custom_palettes
    @css = Sass::Engine.new(TEMPLATE, {
    syntax: :scss,
    style: :expanded,
    load_paths: [
    Rails.root.join('app/assets/stylesheets')
    ],
    custom: { color: params[:custom_color] }
    }).render
    end
    end

    View Slide

  35. class PalettesController < ApplicationController
    TEMPLATE = <<-SCSS.freeze
    $palettes: get-custom-palettes();
    @import 'ui';
    SCSS
    def custom_palettes
    @css = Sass::Engine.new(TEMPLATE, {
    syntax: :scss,
    style: :expanded,
    load_paths: [
    Rails.root.join('app/assets/stylesheets')
    ],
    custom: { color: params[:custom_color] }
    }).render
    end
    end

    View Slide

  36. module Sass::Script::Functions
    def get_custom_palettes
    value = Sass::Script::Value
    color = value::Color.from_hex(options[:custom][:color])
    palettes = 3.times.map do |n|
    factor = value::Number.new((n + 1) * 10, '%')
    value::Map.new({
    value::String.new('bg') => lighten(color, factor),
    value::String.new('text') => darken(color, factor)
    })
    end
    value::List.new(palettes, :space)
    end
    end

    View Slide

  37. module Sass::Script::Functions
    def get_custom_palettes
    value = Sass::Script::Value
    color = value::Color.from_hex(options[:custom][:color])
    palettes = 3.times.map do |n|
    factor = value::Number.new((n + 1) * 10, '%')
    value::Map.new({
    value::String.new('bg') => lighten(color, factor),
    value::String.new('text') => darken(color, factor)
    })
    end
    value::List.new(palettes, :space)
    end
    end

    View Slide

  38. module Sass::Script::Functions
    def get_custom_palettes
    value = Sass::Script::Value
    color = value::Color.from_hex(options[:custom][:color])
    palettes = 3.times.map do |n|
    factor = value::Number.new((n + 1) * 10, '%')
    value::Map.new({
    value::String.new('bg') => lighten(color, factor),
    value::String.new('text') => darken(color, factor)
    })
    end
    value::List.new(palettes, :space)
    end
    end

    View Slide

  39. Demo
    sassy-demos.jeremyfairbank.com/palettes

    View Slide

  40. View Slide

  41. GET /palettes/custom.css?
    color=steelblue

    View Slide

  42. GET /palettes/custom.css?
    color=steelblue

    View Slide

  43. CSS

    View Slide

  44. CSS

    View Slide

  45. ~2s Response Times!!!

    View Slide

  46. Sass Rendering is Slow
    (1-1.5s in this case).

    View Slide

  47. Sass Rendering is Slow
    (1-1.5s in this case).
    Data Structure Access

    View Slide

  48. Sass Rendering is Slow
    (1-1.5s in this case).
    Data Structure Access
    Compass Functions

    View Slide

  49. Sass Rendering is Slow
    (1-1.5s in this case).
    Data Structure Access
    Compass Functions
    Complex CSS

    View Slide

  50. Sass Rendering is Slow
    (1-1.5s in this case).
    Data Structure Access
    Compass Functions
    Loops
    Complex CSS

    View Slide

  51. Sass Rendering is Slow
    (1-1.5s in this case).
    Data Structure Access
    Compass Functions
    Many File
    Dependencies
    Loops
    Complex CSS

    View Slide

  52. View Slide

  53. GET /palettes/
    custom.css?
    color=steelblue

    View Slide

  54. GET /palettes/
    custom.css?
    color=steelblue
    GET /palettes/
    custom.css?
    color=red

    View Slide

  55. ?
    GET /palettes/
    custom.css?
    color=steelblue
    GET /palettes/
    custom.css?
    color=red
    GET /

    View Slide

  56. Threading Issues
    Slow render times + GIL = Bad time
    for all

    View Slide

  57. Solutions to improve
    performance?

    View Slide

  58. Unnecessary Re-rendering

    View Slide

  59. Unnecessary Re-rendering
    GET /palettes/
    custom.css?
    color=steelblue
    CSS

    View Slide

  60. Unnecessary Re-rendering
    GET /palettes/
    custom.css?
    color=steelblue
    CSS
    GET /palettes/
    custom.css?
    color=steelblue
    CSS

    View Slide

  61. Unnecessary Re-rendering
    GET /palettes/
    custom.css?
    color=steelblue
    CSS
    GET /palettes/
    custom.css?
    color=steelblue
    GET /palettes/
    custom.css?
    color=steelblue
    CSS
    CSS

    View Slide

  62. Caching
    • Why re-render the same stylesheet for a
    given color?
    • Render first time, cache stylesheet, and
    serve future requests via cache.
    • Memcache (memcached.org)
    • Dalli (github.com/mperham/dalli)

    View Slide

  63. # config/environments/production.rb
    config.cache_store =
    :mem_cache_store,
    MEM_CACHE_SERVER, MEM_CACHE_OPTIONS
    # Simple usage
    Rails.cache.write('sass', 'is awesome')
    Rails.cache.fetch('sass') == 'is awesome'
    # Using with Sass rendering
    @css = Rails.cache.fetch('palettes/steelblue') do
    Sass::Engine.new(...).render
    end

    View Slide

  64. Using Caching
    $

    View Slide

  65. Using Caching
    GET /palettes/
    custom.css?
    color=steelblue
    $

    View Slide

  66. Using Caching
    GET /palettes/
    custom.css?
    color=steelblue
    CSS
    $
    Miss

    View Slide

  67. Using Caching
    GET /palettes/
    custom.css?
    color=steelblue
    CSS
    $
    Miss
    Write back

    View Slide

  68. Using Caching
    GET /palettes/
    custom.css?
    color=steelblue
    CSS
    GET /palettes/
    custom.css?
    color=steelblue
    CSS
    $
    Miss
    Write back

    View Slide

  69. Using Caching
    GET /palettes/
    custom.css?
    color=steelblue
    CSS
    GET /palettes/
    custom.css?
    color=steelblue
    GET /palettes/
    custom.css?
    color=steelblue
    CSS
    CSS
    $
    Miss
    Write back

    View Slide

  70. Drastically Improve
    Response Times
    100-200ms for subsequent requests
    for same color.

    View Slide

  71. Dealing with a new color
    request
    • Can’t improve render time, but free up
    thread quickly.
    • Make rendering asynchronous.
    • Utilize workers in the background.
    • Sidekiq (sidekiq.org)

    View Slide

  72. $

    View Slide

  73. $
    GET /palettes/
    custom.css?
    color=steelblue

    View Slide

  74. $
    GET /palettes/
    custom.css?
    color=steelblue
    Render CSS

    View Slide

  75. $
    GET /palettes/
    custom.css?
    color=steelblue
    Key:
    palette/
    steelblue
    Render CSS

    View Slide

  76. $
    CSS
    Loading…

    View Slide

  77. $
    Write to
    cache
    CSS
    Loading…

    View Slide

  78. $
    Write to
    cache
    Notify
    CSS
    Loading…

    View Slide

  79. $
    CSS
    Loading…
    GET /palettes/
    by_key.css?
    key=palette
    %2Fsteelblue

    View Slide

  80. $
    CSS
    GET /palettes/
    by_key.css?
    key=palette
    %2Fsteelblue

    View Slide

  81. # app/workers/sass_custom_palettes_worker.rb
    class SassCustomPalettesWorker
    include Sidekiq::Worker
    def perform(color)
    SassCustomPalettes.new(color).render
    end
    end

    View Slide

  82. class SassCustomPalettes
    def initialize(color)
    @color = color
    @key = generate_client_key(color)
    end
    def render
    css = Sass::Engine.new(
    DEFAULTS.merge(custom: { color: @color })
    ).render
    Rails.cache.write(@key, css)
    end
    def render_async
    unless Rails.cache.exist? @key
    SassCustomPalettesWorker.peform_async(color)
    end
    @key
    end
    end

    View Slide

  83. class SassCustomPalettes
    def initialize(color)
    @color = color
    @key = generate_client_key(color)
    end
    def render
    css = Sass::Engine.new(
    DEFAULTS.merge(custom: { color: @color })
    ).render
    Rails.cache.write(@key, css)
    end
    def render_async
    unless Rails.cache.exist? @key
    SassCustomPalettesWorker.peform_async(color)
    end
    @key
    end
    end

    View Slide

  84. class SassCustomPalettes
    def initialize(color)
    @color = color
    @key = generate_client_key(color)
    end
    def render
    css = Sass::Engine.new(
    DEFAULTS.merge(custom: { color: @color })
    ).render
    Rails.cache.write(@key, css)
    end
    def render_async
    unless Rails.cache.exist? @key
    SassCustomPalettesWorker.peform_async(color)
    end
    @key
    end
    end
    Called by controller

    View Slide

  85. class SassCustomPalettes
    def initialize(color)
    @color = color
    @key = generate_client_key(color)
    end
    def render
    css = Sass::Engine.new(
    DEFAULTS.merge(custom: { color: @color })
    ).render
    Rails.cache.write(@key, css)
    end
    def render_async
    unless Rails.cache.exist? @key
    SassCustomPalettesWorker.peform_async(color)
    end
    @key
    end
    end
    Called by controller

    View Slide

  86. class SassCustomPalettes
    def initialize(color)
    @color = color
    @key = generate_client_key(color)
    end
    def render
    css = Sass::Engine.new(
    DEFAULTS.merge(custom: { color: @color })
    ).render
    Rails.cache.write(@key, css)
    end
    def render_async
    unless Rails.cache.exist? @key
    SassCustomPalettesWorker.peform_async(color)
    end
    @key
    end
    end
    Called by controller

    View Slide

  87. class SassCustomPalettes
    def initialize(color)
    @color = color
    @key = generate_client_key(color)
    end
    def render
    css = Sass::Engine.new(
    DEFAULTS.merge(custom: { color: @color })
    ).render
    Rails.cache.write(@key, css)
    end
    def render_async
    unless Rails.cache.exist? @key
    SassCustomPalettesWorker.peform_async(color)
    end
    @key
    end
    end
    Called by controller

    View Slide

  88. class SassCustomPalettes
    def initialize(color)
    @color = color
    @key = generate_client_key(color)
    end
    def render
    css = Sass::Engine.new(
    DEFAULTS.merge(custom: { color: @color })
    ).render
    Rails.cache.write(@key, css)
    end
    def render_async
    unless Rails.cache.exist? @key
    SassCustomPalettesWorker.peform_async(color)
    end
    @key
    end
    end

    View Slide

  89. Downsides
    • Increased code complexity.
    • Introduce polling client or websocket code.
    • Dealing with failing jobs and retries.
    • Waiting for a free worker.
    • Increased network traffic.
    • Latency for client.

    View Slide

  90. Can we do better?

    View Slide

  91. Refactoring
    • Simplify CSS rules.
    • Limit nesting.
    • Limit looping.
    • Remove redundant @import's.
    • Remove Compass.
    • Move to libsass (github.com/sass/sassc-ruby)

    View Slide

  92. Response Time 4x Faster
    ~500ms
    Cached Response Time
    ~150-200ms

    View Slide

  93. Takeaways
    • Sass and Ruby interoperability to
    generate dynamic CSS.
    • Even Sass can impact server
    performance.
    • Good practices for server performance:
    caching and asynchronous processing.
    • Clean, simple Sass can render quicker.

    View Slide

  94. Thanks!
    Demo:

    sassy-demos.jeremyfairbank.com/palettes
    Demo Repo:

    github.com/jfairbank/dynamically-sassy-demos
    Code Samples:
    github.com/jfairbank/dynamically-sassy-talk

    Jeremy Fairbank
    @elpapapollo
    jeremyfairbank.com

    View Slide