Search code examples
ruby-on-railsruby-on-rails-3.1capistranoasset-pipeline

Speed up assets:precompile with Rails 3.1/3.2 Capistrano deployment


My deployments are slow, they take at least 3 minutes. The slow Capistrano task during deploy is assets:precompile. This takes probably 99% of the total deploy time. How can I speed this up? Should I precompile my assets on my local machine and add them to my git repo?

Edit: Adding config.assets.initialize_on_precompile = false to my application.rb file dropped the precompile time with half a minute, but it is still slow.


Solution

  • The idea is that if you don't change your assets you don't need to recompile them each time:

    This is the solution that Ben Curtis propose for a deployment with git:

     namespace :deploy do
          namespace :assets do
            task :precompile, :roles => :web, :except => { :no_release => true } do
              from = source.next_revision(current_revision)
              if releases.length <= 1 || capture("cd #{latest_release} && #{source.local.log(from)} vendor/assets/ app/assets/ | wc -l").to_i > 0
                run %Q{cd #{latest_release} && #{rake} RAILS_ENV=#{rails_env} #{asset_env} assets:precompile}
              else
                logger.info "Skipping asset pre-compilation because there were no asset changes"
              end
          end
        end
      end
    

    Here is another approach based on asset age (https://gist.github.com/2784462) :

    set :max_asset_age, 2 ## Set asset age in minutes to test modified date against.
    
    after "deploy:finalize_update", "deploy:assets:determine_modified_assets", "deploy:assets:conditionally_precompile"
    
    namespace :deploy do
      namespace :assets do
    
        desc "Figure out modified assets."
        task :determine_modified_assets, :roles => assets_role, :except => { :no_release => true } do
          set :updated_assets, capture("find #{latest_release}/app/assets -type d -name .git -prune -o -mmin -#{max_asset_age} -type f -print", :except => { :no_release => true }).split
        end
    
        desc "Remove callback for asset precompiling unless assets were updated in most recent git commit."
        task :conditionally_precompile, :roles => assets_role, :except => { :no_release => true } do
          if(updated_assets.empty?)
            callback = callbacks[:after].find{|c| c.source == "deploy:assets:precompile" }
            callbacks[:after].delete(callback)
            logger.info("Skipping asset precompiling, no updated assets.")
          else
            logger.info("#{updated_assets.length} updated assets. Will precompile.")
          end
        end
    
      end
    end
    

    If you prefer to precompile your assets locally you can use this task:

    namespace :deploy do
      namespace :assets do
        desc 'Run the precompile task locally and rsync with shared'
        task :precompile, :roles => :web, :except => { :no_release => true } do
          from = source.next_revision(current_revision)
          if releases.length <= 1 || capture("cd #{latest_release} && #{source.local.log(from)} vendor/assets/ app/assets/ | wc -l").to_i > 0
            %x{bundle exec rake assets:precompile}
            %x{rsync --recursive --times --rsh=ssh --compress --human-readable --progress public/assets #{user}@#{host}:#{shared_path}}
            %x{bundle exec rake assets:clean}
          else
            logger.info 'Skipping asset pre-compilation because there were no asset changes'
          end
        end
      end
    end 
    

    Another interesting approach can be that of using a git hook. For example you can add this code to .git/hooks/pre-commit which checks if there are any differences in the assets files and eventually precompiles them and add them to the current commit.

    #!/bin/bash
    
    # source rvm and .rvmrc if present
    [ -s "$HOME/.rvm/scripts/rvm" ] && . "$HOME/.rvm/scripts/rvm"
    [ -s "$PWD/.rvmrc" ] && . "$PWD/.rvmrc"
    
    # precompile assets if any have been updated
    if git diff-index --name-only HEAD | egrep '^app/assets' >/dev/null ; then
      echo 'Precompiling assets...'
      rake assets:precompile:all RAILS_ENV=production RAILS_GROUPS=assets
      git add public/assets/*
    fi
    

    If you decide to use this approach you would probably need to change your config/environments/development.rb adding:

    config.assets.prefix = '/assets_dev'
    

    So that while in development you won't serve the precompiled assets.