Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

How to allow site theme color customization?

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

GET /palettes/custom.css? color=steelblue

Slide 9

Slide 9 text

GET /palettes/custom.css? color=steelblue

Slide 10

Slide 10 text

CSS

Slide 11

Slide 11 text

CSS

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

$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); } }

Slide 15

Slide 15 text

$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

Slide 16

Slide 16 text

$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

Slide 17

Slide 17 text

$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

Slide 18

Slide 18 text

$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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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); } }

Slide 21

Slide 21 text

// 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); } }

Slide 22

Slide 22 text

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';

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

# 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

Slide 25

Slide 25 text

# 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

Slide 26

Slide 26 text

# 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

Slide 27

Slide 27 text

# 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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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.

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Demo sassy-demos.jeremyfairbank.com/palettes

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

GET /palettes/custom.css? color=steelblue

Slide 42

Slide 42 text

GET /palettes/custom.css? color=steelblue

Slide 43

Slide 43 text

CSS

Slide 44

Slide 44 text

CSS

Slide 45

Slide 45 text

~2s Response Times!!!

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

No content

Slide 53

Slide 53 text

GET /palettes/ custom.css? color=steelblue

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Solutions to improve performance?

Slide 58

Slide 58 text

Unnecessary Re-rendering

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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)

Slide 63

Slide 63 text

# 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

Slide 64

Slide 64 text

Using Caching $

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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)

Slide 72

Slide 72 text

$

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

$ CSS Loading…

Slide 77

Slide 77 text

$ Write to cache CSS Loading…

Slide 78

Slide 78 text

$ Write to cache Notify CSS Loading…

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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.

Slide 90

Slide 90 text

Can we do better?

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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.

Slide 94

Slide 94 text

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