dynamically-sassy-talk



dynamically-sassy-talk

1 1


dynamically-sassy-talk


On Github jfairbank / dynamically-sassy-talk

Dynamically Sassy

Generating Dynamic CSS in Rails

Jeremy Fairbank

What are we going to talk about?

Generate dynamic CSS

Write Sass functions in Ruby

Identify performance issues

Caching and background processing

Hi, I'm Jeremy

...or

@elpapapollo

pushagency.io

simplybuilt.com

Our Problem

Implementation Issues

Same Sass style sheet for default and custom color palettes

Rendering custom color style sheets from a controller

Slow Sass render times

Caching generated CSS

Rendering in a worker

Syntactically Awesome Style Sheets

sass-lang.com

    // Variables
    $font-size: 12px;

    // Nested rules
    #foo {
      .bar {
        font-size: $font-size;
      }
    }

    // Include other Sass style sheets
    @import 'bar';

    // Mixins
    @mixin has-cats {
      &::before {
        content: 'meow';
        display: block;
      }
    }

    #internet {
      @include has-cats;
    }
          

So What?

Modularity

DRY

Data Structures

Scripting

Color Functions

That's Sass

Reusing style sheets

It's all in the variables

Static Context

    // _my-ui.scss
    #foo {
      background: nth($palette, 1);
      color: nth($palette, 2);
    }
          
    // palette1.scss
    $palette: (red green);
    @import 'my-ui';
          
    // palette2.scss
    $palette: (blue yellow);
    @import 'my-ui';
          

Dynamic Context

Sass Functions

    // dynamic-palette.scss
    $palette: get-dynamic-palette();
    @import 'my-ui';
          
    # config/initializers/sass.rb
    module Sass::Script::Functions
      def get_dynamic_palette
        palette = 2.times.map do
          color = '#%06x' % (rand * 0xffffff)
          Sass::Script::Value::Color.from_hex(color)
        end

        Sass::Script::Value::List.new(palette, :space)
      end
    end
          

Injecting data from a user?

    // app/assets/stylesheets/dynamic-palette-rails.css.scss
    $palette: get-dynamic-palette-from-user-somehow-magically();
    @import 'my-ui';
          

Use the asset pipeline?

"Static" context in the tilt template

  # sass-rails/lib/sass/rails/template.rb
  module Sass
    module Rails
      class SassTemplate < Tilt::Template
        # ...
        def evaluate(context, locals, &block)
          cache_store = CacheStore.new(context.environment)

          options = {
            :filename => eval_file,
            :line => line,
            :syntax => syntax,
            :cache_store => cache_store,
            :importer => importer_class.new(context.pathname.to_s),
            :load_paths => context.environment.paths.map { |path| importer_class.new(path.to_s) },
            :sprockets => {
              :context => context,
              :environment => context.environment
            }
          }

          sass_config = context.sass_config.merge(options)

          engine = ::Sass::Engine.new(data, sass_config)
          css = engine.render

          engine.dependencies.map do |dependency|
            context.depend_on(dependency.options[:filename])
          end

          css
        rescue ::Sass::SyntaxError => e
          context.__LINE__ = e.sass_backtrace.first[:line]
          raise e
        end
        # ...
      end
    end
  end
          

Oh, and assets are precompiled for production

We need our own render class for dynamic content

Sass::Engine

to the rescue

    # lib/sass_custom_palette.rb
    class SassCustomPalette
      TEMPLATE = <<-EOS.freeze
        $palette: get-custom-palette();
        @import 'my-ui';
      EOS

      def initialize(color)
        @color = color
      end

      def render
        Sass::Engine.new(TEMPLATE, sass_custom_options).render
      end

      private

      def sass_custom_options
        { syntax: :scss,
          style: :expanded,
          custom: { color: @color } }
      end
    end
          
    # config/initializers/sass.rb
    module Sass::Script::Functions
      def get_custom_palette
        color = Sass::Script::Value::Color.from_hex(
          options[:custom][:color]
        )

        factor = Sass::Script::Value::Number.new(20, '%')

        palette = [
          lighten(color, factor),
          darken(color, factor)
        ]

        Sass::Script::Value::List.new(palette, :space)
      end
    end
          
    # app/controllers/palettes_controller.rb
    class PalettesController < ApplicationController
      def custom_palette
        custom_renderer = SassCustomPalette.new(
          params[:custom_color]
        )

        @css = custom_renderer.render
      end
    end
          
    -# app/views/palettes/custom_palette.css.haml
    = @css.html_safe
          

Try it out...

    Sass::SyntaxError - File to import not found or unreadable: my-ui.
          

-__-

There's always a load path

    class SassCustomPalette
      private

      def load_paths
        root = Rails.root.join('app', 'assets', 'stylesheets')
        Dir[root.join('includes')]
      end
      
      def sass_custom_options
        { syntax: :scss,
          style: :expanded,
          load_paths: load_paths,
          custom: { color: @color } }
      end
    end
          
    // app/assets/stylesheets/includes/_my-ui.scss
    #foo {
      background: nth($palette, 1);
      color: nth($palette, 2);
    }
          

Dynamic Sass Demo

sassy-demos.jeremyfairbank.com/palettes

Implementing in SimplyBuilt

GET

/custom_palette.css?custom_color=%23f00

...waiting

...waiting

...waiting

Done!

What's the problem?

Slow render times

(1-1.5 seconds!)

Complex Sass rules

Many file dependencies

Loops

Data structure lookups

Compass functions

Can impact the whole server

Web Server

Web Server

Web Server

Web Server

Web Server

Yup, we need caching

Memcached

[memcached.org](http://memcached.org) / [github.com/mperham/dalli](https://github.com/mperham/dalli)

    # config/environments/production.rb
    config.cache_store = :mem_cache_store, \
      MEM_CACHE_SERVER, MEM_CACHE_OPTIONS
          
    Rails.cache.write('foo', 'bar')

    Rails.cache.fetch('foo') #=> 'bar'
          

Introduce caching into our Sass rendering

    class SassRenderer
      def initialize(template, cache_key, options)
        @cache_key = cache_key
        @engine = Sass::Engine.new(template, options)
      end

      def render
        from_cache { @engine.render }
      end

      def cached?
        Rails.cache.exist?(@cache_key)
      end

      def get_cached_css
        Rails.cache.fetch(@cache_key)
      end

      private

      def set_cached_css(css)
        Rails.cache.write(@cache_key, css)
      end

      def from_cache(&write_block)
        get_cached_css || write_block.call.tap { |css| set_cached_css(css) }
      end
    end
          

Incorporate into

SassCustomPalette

    class SassCustomPalette
      def initialize(color)
        @color = color

        @cache_key = "custom_palette/#{@color}"

        @engine = SassRenderer.new(
          TEMPLATE, @cache_key, sass_custom_options
        )
      end

      delegate :render, to: :@engine
    end
          

And?

What about the first render?

Process in the background

sidekiq.org

Why Bother?

Free up server thread quickly to handle new request

No thread concurrency guarantees with MRI

But...

Code complexity

Polling client

Websockets

Failing jobs

Waiting on an available worker

Network traffic

Setting up a Sidekiq worker

    # app/workers/sass_custom_palette_worker.rb
    class SassCustomPaletteWorker
      include Sidekiq::Worker

      def perform(color)
        SassCustomPalette.new(color).render
      end
    end
          
    class SassCustomPalette
      def render_async
        unless @engine.cached?
          SassCustomPaletteWorker.peform_async(@color)
        end

        @cache_key
      end
    end
          
    class SassRenderer
      def self.get_by_key(key)
        Rails.cache.fetch(key)
      end

      def self.cached?(key)
        Rails.cache.exist?(key)
      end

      private

      def get_cached_css
        self.class.get_by_key(@cache_key)
      end
    end
          

What would polling look like?

Polling

Polling

Polling

Polling

Polling

Polling

Polling

Polling

    class PalettesController < ApplicationController
      def request_custom_palette
        custom_renderer = SassCustomPalette.new(
          params[:custom_color]
        )

        @key = custom_renderer.render_async
      end

      def check_custom_palette
        @ready = SassRenderer.cached?(params[:key])
      end

      def custom_palette
        @css = SassRenderer.get_by_key(params[:key])
      end
    end
          

Is this the best answer?

Can refactoring simplify our Sass?

YES!

Identified areas for code improvement

Acceptable render times

Eliminate need for background processing

Recap

Reuse Sass style sheets to render static and dynamic content

Sass rendering isn't always fast

Web server performance

Importance of caching

Background processing

Sometimes refactoring is all you need

So what does this mean?

Things aren't always simple

We always have to consider performance

Thanks!

Slides: sassy-talk.jeremyfairbank.com Demo Source: github.com/jfairbank/dynamically-sassy-demos Blog: blog.jeremyfairbank.com

@elpapapollo jeremy@pushagency.io github.com/jfairbank