ConnectJS Dynamically Sassy

ConnectJS Dynamically Sassy

94bd558238b69c45d3d3e15797ae94f7?s=128

Jeremy Fairbank

October 16, 2015
Tweet

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