rcmdnk's blog

High Performance Web Sites: Essential Knowledge for Frontend Engineers

Octopressで気になってる部分を改善しておこうと思って色々やってるんですが、 今回はJavaScriptやHTMLを圧縮したりまとめたりする方法について。

JavaScriptを圧縮

Octopressでは通常source/javascriptsというディレクトリに JavaScriptを置いて、それがroot_url/javascripts/に コピーされるのでそれをhead内とかで呼んでいます。

基本、細かく分けて呼ぶよりもまとめてしまって一度に読んだ方が速いので そうしてみます。

さらに圧縮もかけることにします。

こんな感じのタスクをRakefileの中に用意。

Rakefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
require "yui/compressor"
require "parallel"

js_for_combine   = ['footnote.js', 'jquery.githubRepoWidget.min.js', 'monthly_archive.js', 'utils.js', 'randomposts.js']
js_output        = "all.js"
js_minify_others = false
n_cores          = 4


desc "Minify JS"
task :minify_js do
  puts "## Minifying JS"
  Rake::Task[:combine_js].execute
  if js_minify_others
    Rake::Task[:minify_other_js].execute
  end
end


desc "Minify JS and combine"
task :combine_js do
  puts "## Combining JS"
  compressor = YUI::JavaScriptCompressor.new
  if File.exist?("#{source_dir}/javascripts/#{js_output}")
    t_alljs = File.mtime("#{source_dir}/javascripts/#{js_output}")
    time_check = false
    js_for_combine.each do |j|
      if File.mtime("#{source_dir}/javascripts/#{j}") > t_alljs
        puts "newer file #{j} is found"
        time_check = true
        break
      end
    end
    if not time_check
      next
    end
  end
  output = File.open("#{source_dir}/javascripts/#{js_output}", "w")
  js_for_combine.each do |j|
    input = File.read("#{source_dir}/javascripts/#{j}")
    output << compressor.compress(input)
  end
  output.close
  cp_r "#{source_dir}/javascripts/#{js_output}", "#{public_dir}/javascripts/"
end

desc "Minify other JS"
task :minify_other_js do
  puts "## Minifying other JS"
  n = 0
  compressor = YUI::JavaScriptCompressor.new
  Parallel.map(Dir.glob("#{source_dir}/javascripts/**/*.js"), :in_threads => n_cores)  do |j|
    if (js_for_combine+[js_output]).include?(j.sub("#{source_dir}/javascripts/", "")) or\
       j.include?("compressed")
      next
    end
    d = j.split('/')[0..-2].join('/')
    n = j.split('/')[-1]
    compressed = d + "/compressed/" + n
    if File.directory?(d+"/compressed/")
      if File.file?(compressed) and File.mtime(compressed) > File.mtime(j)
        next
      end
    else
      mkdir_p compressed.split('/')[0..-2].join('/')
    end

    puts "Minifying #{j}"
    input = File.read(j)
    output = File.open(compressed, "w")
    output << compressor.compress(input)
    output.close
    n+=1
  end
  if n > 0
    cp_r "#{source_dir}/javascripts/.", "#{public_dir}/javascripts/"
  end
end

js_for_combineがまとめるJavaScript達、 js_outputがまとめた物を出力するファイル名です。

上のタスクを実行すればsource/_includes/head.html等で all.jsだけを読みこめば良いことになります。

タスクは3つあって、

  • minify_js: これをgenerateタスクの中で呼ぶ。
  • combine_js: 指定されたファイルたちを圧縮してまとめる。
  • minify_other_js: 指定されて無いファイルたちの圧縮したファイルも作成する。

と言った所。

minify_jsの中ではjs_minify_othersを見て minify_other_jsをするかどうか決めています。

圧縮にはYUI Compressorを使っています 1

comibine_jsの方ではインプットの中に1つでも js_outputより新しいファイルがあれば作りなおす、という風になっています。

また、ファイルの作成はsource/javascriptsの中で行い、 出来たファイルをpublic_dirに送る様にしています。

これは単独でこのタスクを動かした時に そのままjekyllコマンドを打たなくても確認したりデプロイ出来る様にするためです。

ちょっとjekyllコマンド内のコピーとやタスク同士でも重複する所が ありますが、問題にならないレベルなので良しと。

後、minify_other_jsの方では各ファイルを別個に圧縮していくので、 Parallel を使って並列処理をしています。 n_coresは並列で走らせる数の設定。

これで後は

<script src="{{root_url}}/javascripts/all.js"></script>

と呼んで上げれば必要な物は全部入ります(全部をリストに入れていれば)。

新しいJavaScriptを追加した場合でも Rakefileのリストに追加すれば自動的に加わるので head.htmlを直接いじるよりも管理はし易いかな、とも思います。

HTMLを圧縮

HTMLに関してはpublic_dirに出力されたものに対して行います。

Rakefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
require "yui/compressor"
require "htmlcompressor"
require "parallel"
require 'ruby-progressbar'

html_for_minify = ["*.html", "blog/*/*/*/*/*.html", "windows", "mac"]
html_not_minify = ["rawhtml", "others", "test"]
n_cores          = 4

desc "Minify HTML"
task :minify_html do
  puts "## Minifying HTML"

  option={
     :remove_comments => false,
     :compress_css => true,
     :css_compressor => :yui,
     :compress_javascript => true,
     :javascript_compressor => :yui
  }
  compressor = HtmlCompressor::Compressor.new(option)

  posts = []
  if html_for_minify == "all" or html_for_minify[0] == "all"
    posts = Dir.glob("#{public_dir}/**/*.html")
  else
    posts_tmp = html_for_minify
    if html_for_minify.is_a?(String)
      posts_tmp = [posts_tmp]
    end
    posts_tmp.each do |p|
      f = "#{public_dir}/#{p}"
      if f.scan("\*").length > 0
        posts += Dir.glob(f)
      elsif File.file?(f)
        posts.push(f)
      elsif File.directory?(f)
        posts += Dir.glob("#{f}/**/*.html")
      end
    end
  end
  if html_not_minify != nil
    if html_not_minify.is_a?(String)
      html_not_minify = [html_not_minify]
    end
    html_not_minify.each  do |p|
      posts.delete_if {|post| post.start_with?("#{public_dir}/#{p}")}
    end
  end

  progressbar = ProgressBar.create(:title => "Minify HTML", :starting_at => 0,
                                   :total => posts.size,
                                   :format => '%t, %a |%b%i| %p%')
  Parallel.map(posts, :in_threads => n_cores)  do |p|
    input = File.read(p)
    output = File.open(p, "w")
    output << compressor.compress(input)
    output.close
    progressbar.increment
  end
end

html_for_minifyで圧縮する物を決めます。 文字列でも配列でも良くて、 与えられた物がディレクトリならその中を再帰的に、 ファイルならそれを圧縮します。

指定はルートディレクトリより下からで(public_dirからのパス)。

allと言う文字列を与えると全てのHTMLファイルを探して圧縮します。

また、逆に特定のものだけを除きたい場合には html_for_minifyに入れます。 こちらもディレクトリの場合は再帰的に除きます。

HTMLにはHtmlCompressor を使います。 これにはGoogleの htmlcompressor をベースにしたものが入っています。 従ってオプションもGoogleの物を参考に出来ます。

与えているオプションは コメントを残す事と、HTML内に直接書かれたCSS/JavaScriptに関してYUIの物を使って 圧縮する事を指定しています。

コメントに関しては Octopressで’ダブルハイフン’をそのまま残す でも触れたように、一部コメント内を見て判断する外部サービスとかもあるので 残すことが必要です。

preserve_patternsというオプションを使うと、 決められたパターンだけ残せるので 特定のコメントだけ残すことも可能ですが、 全て残しても大した量ではないので間違えて消すリスクを考えたら 全部残しておいた方が良いな、ということで全部残すようにしています。

後、これ結構時間がかかるので、 ruby-progressbar を使って実行経過を表示するようにもしました。 (ruby-progressbarに関してはGitHubのWiki に詳しく使い方が書いてあります。)

これで

Minify HTML, Time: 00:01:26 |=======================================       | 85%

な感じの表示が出ます。

CSSは?

CSSもYUIのCssCompressorを使って圧縮することが出来ますが、 OctopressではCSSをcompassを使ってコンパイルして まとめてるので別途行う必要はないと思います。

もしやりたいなら

Rakefile lang:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require "yui/compressor"
require "parallel"
n_cores = 4

desc "Minify CSS"
task :minify_css do
  puts "## Minifying CSS"
  compressor = YUI::CssCompressor.new
  Parallel.map(Dir.glob("#{source_dir}/stylesheets/**/*css"), :in_threads=> n_cores) do |name|
    puts "Minifying #{name}"
    input = File.read(name)
    output = File.open("#{name}", "w")
    output << compressor.compress(input)
    output.close
  end
  cp_r "#{source_dir}/stylesheets/.", "#{public_dir}/stylesheets/"
end

な感じ。試しにやってみたら compassの出力だと59027文字だったscreen.cssが58707文字まで減りました。

一応まだ圧縮できる事は出来るみたいです。

まとめ

上のようにして圧縮出来る様になったので、後は generateタスクの中とかで

Rakefile
1
2
3
4
5
6
7
8
9
10
11
  system "compass compile #{style} --css-dir #{source_dir}/stylesheets"
  system "jekyll build"

  # Minify JavaScript
  Rake::Task[:minify_js].execute

  # Fix double dash problem
  Rake::Task[:fix_double_dash].execute

  # Compress HTML
  Rake::Task[:minify_html].execute

みたいな感じでjekyll buildした後に呼んであげればOK。 minify_htmlの方は Octopressで’ダブルハイフン’をそのまま残す のとこで作ったfix_double_dashよりは後にかけないといけません。

後は上のコードのrequireの所をRakefileの先頭に、 js_for_combine等のオプションを先頭の方にあるオプション群に追加、 タスクを適当な所に書いておけばOK。

参考:

Octopress: Minify HTML, CSS and JS · Andrei Mihu

Sponsored Links
Sponsored Links

« Octopress (Liquid)のfor文内等でタグを使う時の注意 ソーシャルボタンの数をビルド時に取ってくる »

}