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. Challenges • Generate stylesheet from user input. • Avoid duplication

    between dynamic and static stylesheets. • Performance.
  2. CSS

  3. CSS

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

    Sass engine rendering. • Web server performance. • Caching and background processing. • Refactoring.
  5. $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); } }
  6. $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
  7. $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
  8. $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
  9. $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
  10. 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
  11. 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); } }
  12. // 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); } }
  13. 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';
  14. # 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
  15. # 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
  16. # 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
  17. # 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
  18. 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.
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. CSS

  29. CSS

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

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

    Access Compass Functions Many File Dependencies Loops Complex CSS
  32. Unnecessary Re-rendering GET /palettes/ custom.css? color=steelblue CSS GET /palettes/ custom.css?

    color=steelblue GET /palettes/ custom.css? color=steelblue CSS CSS
  33. 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)
  34. # 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
  35. 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
  36. 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)
  37. $

  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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.
  46. Refactoring • Simplify CSS rules. • Limit nesting. • Limit

    looping. • Remove redundant @import's. • Remove Compass. • Move to libsass (github.com/sass/sassc-ruby)
  47. 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.