Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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
  2. Hi, I’m Jeremy jfairbank @elpapapollo blog.jeremyfairbank.com

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

  4. None
  5. How to allow site theme color customization?

  6. Challenges • Generate stylesheet from user input. • Avoid duplication

    between dynamic and static stylesheets. • Performance.
  7. None
  8. GET /palettes/custom.css? color=steelblue

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

  10. CSS

  11. CSS

  12. Roadmap • Why Sass? • Sass and Ruby interoperability. •

    Sass engine rendering. • Web server performance. • Caching and background processing. • Refactoring.
  13. Why Sass ? • Modularity • DRY • Loops, functions,

    lists, and maps.
  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); } }
  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
  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
  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
  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
  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
  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); } }
  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); } }
  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';
  23. Dynamic Data? // theme1_dynamic.scss $palettes: get-dynamic-palettes(); @import 'ui';

  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
  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
  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
  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
  28. Dynamic Data from User? // theme1_dynamic_user.scss $palettes: get-dynamic-palettes-from-user-magically(); @import 'ui';

  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.
  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
  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
  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
  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
  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
  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
  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
  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
  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
  39. Demo sassy-demos.jeremyfairbank.com/palettes

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

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

  43. CSS

  44. CSS

  45. ~2s Response Times!!!

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

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

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

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

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

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

    Access Compass Functions Many File Dependencies Loops Complex CSS
  52. None
  53. GET /palettes/ custom.css? color=steelblue

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

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

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

    for all
  57. Solutions to improve performance?

  58. Unnecessary Re-rendering

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

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

    color=steelblue CSS
  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
  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)
  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
  64. Using Caching $

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

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

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

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

    color=steelblue CSS $ Miss Write back
  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
  70. Drastically Improve Response Times 100-200ms for subsequent requests for same

    color.
  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)
  72. $

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

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

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

  76. $ CSS Loading…

  77. $ Write to cache CSS Loading…

  78. $ Write to cache Notify CSS Loading…

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

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

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

    end
  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
  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
  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
  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
  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
  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
  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
  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.
  90. Can we do better?

  91. Refactoring • Simplify CSS rules. • Limit nesting. • Limit

    looping. • Remove redundant @import's. • Remove Compass. • Move to libsass (github.com/sass/sassc-ruby)
  92. Response Time 4x Faster ~500ms Cached Response Time ~150-200ms

  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.
  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