Rails 根据实际的应用场景 Rails 其实还可以做到更极致的优化

huacnlee · 2015年04月24日 · 最后由 huacnlee 回复于 2015年05月26日 · 3555 次阅读

今天我给 Ruby China 做了一个改进,然后每个页面的请求耗时少了 3.5ms

大概是这样的,据之前的统计发现,javascript_include_tag, stylesheet_link_tag 这两个 helper 在 production 环境下面依然会浪费 3ms 左右的时间,3ms 啊,Cache 命中的时候,Ruby China 的列表实际上才 60ms 左右(顺便说一下,去年是 80ms,见这篇帖子)。

并且按说不应该的,Assets Pipeline 都已经编译好了,跑起来以后就不需要计算了啊(至少那个 URL 是不会变的)

于是看了一下 ActionView 里面这个 helper 的实现,发现好复杂的逻辑,并且在那些逻辑前面没用 Cache 的迹象...

actionview/lib/action_view/helpers/asset_tag_helper.rb

def javascript_include_tag(*sources)
  options = sources.extract_options!.stringify_keys
  path_options = options.extract!('protocol', 'extname').symbolize_keys
  sources.uniq.map { |source|
    tag_options = {
      "src" => path_to_javascript(source, path_options)
    }.merge!(options)
    content_tag(:script, "", tag_options)
  }.join("\n").html_safe
end

于是动了一下(当然不能一概而论,Ruby China 适合我后面的做法)

module ApplicationHelper
  $html_cache = {}
  def fetch_cache_html(*keys)
    cache_key = keys.join("/")
    if controller.perform_caching && (html = $html_cache[cache_key])
      return html
    else
      html = yield
      # logger.info "HTML CACHE Missed: #{cache_key}"
      $html_cache[cache_key] = html
    end
    html
  end

  def stylesheet_link_tag_with_cached(name)
    fetch_cache_html("stylesheets_link_tag",name) do
      stylesheet_link_tag(name, 'data-turbolinks-track' => true)
    end
  end

  def javascript_include_tag_with_cached(name)
    fetch_cache_html("javascript_include_tag", name) do
      javascript_include_tag(name, 'data-turbolinks-track' => true)
    end
  end

  def cached_asset_path(name)
    fetch_cache_html("asset_path", name) do
      asset_path(name)
    end
  end
end

stylesheet_link_tag_with_cached 替代原来的,减少复杂的计算,效果有了~

$html_cache 将会在 Rails 进程重启的时候丢掉,在 Assets 这几个 helper 并且参数也简单只有一个文件的情况下,正好可以用进程内存存放 Helper 的 HTML 结果,反复利用。


典型的空间换时间案例


2015-5-26 更新

最终我发现,慢是在 sprockets-rails 这边,它覆盖了 javascript_include_tagstylesheet_link_tag 实现了它的一些机制。

已经根据此文的实现,给 sprockets-rails 提交了 PR 不知道 Rails core team 怎么看

https://github.com/rails/sprockets-rails/pull/253

这个应该是 decorator 模式应用的典型例子吧?

并没有改变 javascript_include_tagstylesheet_link_tag 的行为,但是在其上封装了一层 cache 机制,很巧妙,赞!

我好像想问一下楼主,你当初的 ruby 学到什么程度才去实习的?或者至少要什么样儿别人才要啊?我感觉没人带着学着真的是痛苦不堪!

#2 楼 @hemengzhi88 没实习过,第一份工作的时候能独立完成网站开发,会 ASP, ASP.NET, Delphi 写过两个软件,会做 UI 设计 😄 😄

其实我想说问这个没意义

#1 楼 @lgn21st 不过再快也没用,最后都被网络耗时给毁了

#3 楼 @huacnlee 原来会这么多。。。

把这一大段直接加 view 片段缓存,可以更简单和快一点。

class << Rails
  attr_accessor :memory_cache
end
Rails.memory_cache = ActiveSupport::Cache::MemoryStore.new
== Rails.memory_cache.fetch 'header' do
  = javascript_include_tag ...
  = stylesheet_link_tag ...

开发模式还要禁用掉否则不能 reload

#6 楼 @luikore 我用了土方法 😄

可用 controller.perform_caching 做个简单判断就好了。

正确的应该是基于 Rails.application.config.cache_classes

#6 楼 @luikore 开发模式用 ActiveSupport::Cache::NullStore

我觉得没必要也不应该这么做

还要分别考虑 2 个 helper 的 cache 版本,感觉考虑太多细节了。

不如直接放到同一个 partial 例如‘_assets.html.erb’: (不用 partial 也可以的,只是分离开更好维护)

<%= stylesheet_link_tag "front" %>
<%= javascript_include_tag "app" %>

然后 layout 里用一个 fragment cache:

<%= cache_if Rails.env.production?, $applicaion_release_version { render 'assets' } %>

$applicaion_release_version 是最后一次部署的版本信息,根据具体部署方案自己定义,能保证每次部署缓存失效即可。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号