Sansan Builders Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

まだ間に合う!node-sass(LibSass)から sass(Dart Sass)への移行

こんにちは。 Eight で エンジニアをしている鳥山(@pvcresin)です。
違う違うと自分に言い聞かせていますが、おそらく花粉症になってしまいました 🥺
在宅勤務で良かったです。
今回は Sass のコンパイルに使用しているライブラリを node-sass(LibSass)から sass(Dart Sass)に移行した話をしたいと思います。

Sass 実装三銃士

Sass の実装としては、以下の 3 つがあります。

  • Ruby Sass
  • LibSass
  • Dart Sass

まずは移行作業の話の前に、各実装について簡単に振り返ってみたいと思います。

Ruby Sass

Ruby Sass は Sass の最初の実装でした。 発表された 2006 年当時、Ruby のエコシステムは急成長しており、すぐに多くの人に使われるようになりました。
しかし、徐々に Ruby 製であることに起因するコンパイル速度の遅さや、他のツールとの連携容易性の問題が取り上げられるようになっていきます。 その結果、開発も活発に行われなくなり、Ruby Sass は 2019 年 3 月にサポートを終了しました。

LibSass

Ruby Sass の問題を解決するために、開発されたのが LibSass です。 LibSass は C/C++製で、動作の速さや連携のしやすさを念頭に開発されています。 LibSass 自体はライブラリであり、これを使用するための ラッパー がたくさんの言語で存在しています。 特にフロントエンド開発においては、Node.js 向けの node-sass を使用するのが一時期デファクトだったこともあり、Eight でも node-sass を使用していました。
しかし、複雑な Sass の実装を C++ で開発できる人は少なく、次第に LibSass への機能追加は行われなくなっていきました。 そして、2020 年 10 月に非推奨 となり、今後は新機能の追加が行われないことが決定しています。

Dart Sass

LibSass の次に開発されたのが Dart Sass です。 Dart Sass は Dart 製で、パフォーマンスをある程度担保した上で、連携のしやすさと開発のしやすさを兼ね備えたバランスの良い実装となっています。 スタンドアローンな実行可能ファイルや CLI に加え、Dart から純粋な JavaScript にコンパイルした sass (npm package)も用意されています。 純粋な JavaScript のバージョンはスタンドアローンなものより動作は遅いですが、JavaScript で動作をカスタマイズできるため便利です。 node-sass を使用している場合は、sass を使用するのが最も手軽に Dart Sass へ移行する方法だと思います。 Dart Sass は開発が盛んで、公式としても使用を推奨しています。

「まだ間に合う!」とは

2019 年 10 月、Sass に モジュールシステムという大きな機能追加 が行われました。 その中でも特に注目されたのが、@import に置き換わる @use を用いた Sass ファイルの読み込み方法です。 別の Sass ファイルの変数などを使いたい場合、@import を用いた場合はグローバルスコープでしたが、@use ではファイルスコープになります。
モジュールシステムについては詳しく解説されている記事が無数に存在するためそちらに譲りたいと思います。 現時点では @use@import は共存可能ですが、@import は完全に廃止されることが決定しています。 上記のリリースでは、今後の計画として以下の方針が示されていました。

  • Dart Sass と LibSass の両方がモジュールシステムをサポートしてから 1 年後、または Dart Sass がモジュールシステムをサポートしてから 2 年後のいずれか早い方(遅くとも 2021 年 10 月)に、@import を非推奨にする。
  • 非推奨から 1 年後(遅くとも 2022 年 10 月)に @import のサポートを終了する。

実際には、モジュールシステムが追加される前に LibSass が非推奨になってしまったため、この計画でいうと 2021 年 10 月に @import は非推奨になります。 従って、今のうちに node-sass(LibSass)から sass(Dart Sass)に移行しておけば、後々の @import から @use への乗り換えをスムーズに進めることができます。

移行手順

ここからは実際の移行手順について見ていきます。 もっとも、LibSass が非推奨になったときの記事Dart Sass の使い方紹介ページ に従うだけで簡単に移行することができます。 Eight では webpack 上の sass-loader で node-sass によるコンパイルを行っており、同じ API を提供している sass に乗り換える形をとりました。

1. sass と fibers のインストール

最初に node-sass をアンインストールし、代わりに sass(Dart Sass)をインストールします。 Eight では Yarn を使っているため、 yarn remove node-sass した後に、yarn add -D sass fibers しました。

// package.json のイメージ
{
  "devDependencies": {
-   "node-sass": "5.0.0",
+   "sass": "1.32.5",
+   "fibers": "5.0.0",
  }
}

ここで、同時にインストールしている fibers とは、Dart Sass の README で紹介されていたライブラリで、コルーチンを実現するためのものです。
Dart Sass には同期処理の renderSync() と非同期処理の render() の 2 つの関数がありますが、オーバーヘッドの問題で renderSync() の方が 2 倍速いそうです。 そこで、fibers を使うと render() のパフォーマンス問題を解消してくれるとのことでした。
ちなみに fibers の README には、「fibers はかなり昔の Node.js 向けに作られたもので、使わないで済むのならその方がよい」との記述もありました 🤔 しかし、今回は Dart Sass の言い分に従い、fibers を使うことにしました。

2. sass-loader に sass と fibers の指定を追加

webpack.config.js で sass-loader に Dart Sass と fibers を使う旨を伝えます。

// webpack.config.js の一部抜粋
{
  loader: 'sass-loader',
  options: {
    /* 省略 */
+   implementation: require('sass'),
+   sassOptions: {
+     fiber: require('fibers'),
+   },
  },
},

あとは E2E (End-To-End) テストなどでスタイルに問題がないかを確認して完了です。

ビルド時間への影響調査

コンパイル速度で見ると、Dart Sass の純粋な JavaScript バージョン(sass)は LibSass(node-sass)より遅いはずなので、全体のビルド時間が遅くなる可能性がありそうだと考えました。 また、fibers の効果についても検証してみたかったので、以下の 3 パターンでコンパイルを含むビルド時間を測定してみました。

  1. node-sass
  2. sass
  3. sass + fibers

なお、測定には私のマシン1を使い、開発モードでのビルド時間(5 回平均)を測定しました。

node-sass sass sass + fibers
ビルド時間 (秒) 46.8 46.4 46.6

僅差ではありますが、sass 単体でコンパイルした場合のビルド時間が最も速いという結果になりました。 もしかしたら fibers はあまり効果がないのかもしれません 😇 とはいえ、結果としてはどれもほぼ同程度の時間といって良いと思います。 大幅なパフォーマンス低下にならないようで一安心です。

まとめ

今回は node-sass(LibSass)から sass(Dart Sass)に移行した話をしました。
特に Eight ならではの内容はなかったのですが、最近移行したので Sass の歴史を簡単に振り返りつつ、まとめてみました。
大きな変更の前には往々にして、移行期間が設定されます。 その期間にしっかりと移行できるよう、計画的に準備していくことが大切ですね。


buildersbox.corp-sansan.com


  1. MacBook Pro (15-inch, 2018), 2.9 GHz 6-Core Intel Core i9, RAM 32 GB

© Sansan, Inc.