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

ConnectJS Dynamically Sassy

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on.

ConnectJS Dynamically Sassy

Avatar for Jeremy Fairbank

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.