expressでejsとmarkoを共存させるの画像
芽萌丸プログラミング部 @programming
投稿日 2020/07/31

Node.js

expressでejsとmarkoを共存させる

目次

express のテンプレートエンジンといえば ejs が有名所ですが、 ejs は内部的に同期的な文字列操作を行っており、また今の所(2020/07/31時点)ストリームにも対応していないため、大きめのページを描画するとイベントループを盛大にブロッキングしてしまいます。 しかし、小さなページはこれまでどおり ejs を使いつつ、大きめのページだけはストリームで出力してブロッキングを少なくしたいというニーズは結構あると思います。そのような場面で使えるテンプレートエンジンが marko です。 marko はストリームにも対応している数少ないテンプレートエンジンです。

今回は ejs と marko を express アプリケーション内で共存させる方法をご紹介します。

前提

  • Node.js v12.16.1
  • express v4.17.1
  • ejs v3.1.3
  • marko v4.22.2

パフォーマンス確認

その前に、まずは ejs と marko のパフォーマンス(ブロッキング時間)を比較してみましょう。

実験用のtestプロジェクトを作成し、必要モジュールを全てインストールしておきます:

$ mkdir test && cd test
$ npm init
$ npm install --save express ejs marko lru-cache blocked-at

ejsのブロッキング時間

まずは ejs のパフォーマンスから見てみましょう。

server.js (ejs)

// ブロッキング検出
const blocked = require('blocked-at');
blocked((time, stack) => {
  console.log(`Blocked for ${time}ms, operation started here:`, stack)
}, {
  threshold: 5, // 5ms以上のブロッキングを検出
});

// express setup:
const express = require('express');
const app = express();

// view engine setup:
const ejs = require('ejs');
const LRU = require('lru-cache');
ejs.cache = new LRU(100); // キャッシュも効かせる
app.set('views', './views');
app.set('view engine', 'ejs');

// 巨大なダミーデータ
const data = {
  "list": [
    { "list": ["ここに巨大な配列", ...] },
    { "list": ["ここに巨大な配列", ...] },
    ...
  ],
};

// GET /
app.get("/", function(req, res) {
  res.render("index", data);
});

app.listen(3000);

views/index.ejs

<div>
  <% list.forEach(function(item, idx){ %>
  <div>
    <% item.list.forEach(function(item, idx){ %>
    <span><%= item %></span>
    <% }); %>
  </div>
  <% }); %>
</div>

<div>
  <% list.forEach(function(item, idx){ %>
  <div>
    <% item.list.forEach(function(item, idx){ %>
    <span><%= item %></span>
    <% }); %>
  </div>
  <% }); %>
</div>

<div>
  <% list.forEach(function(item, idx){ %>
  <div>
    <% item.list.forEach(function(item, idx){ %>
    <span><%= item %></span>
    <% }); %>
  </div>
  <% }); %>
</div>

同じデータを無駄にループしてレンダリングしています。

server.jsを実行し http://localhost:3000/ へアクセスすると、サーバサイドのログに以下のように出力されました:

...
Blocked for 69.56158299970627ms, operation started here: [
  '    at Server.connectionListener (_http_server.js:396:3)',
  '    at Server.emit (events.js:311:20)',
  '    at TCP.onconnection (net.js:1554:8)'
]

ブロッキング時間 (ejs): 69.56158299970627ms

2回目以降のアクセスでキャッシュが効いた状態でだいたいコレぐらいの数値でした。

markoのブロッキング時間

次に marko のパフォーマンスを見てみましょう。

server.js (marko)

// ブロッキング検出
const blocked = require('blocked-at');
blocked((time, stack) => {
  console.log(`Blocked for ${time}ms, operation started here:`, stack)
}, {
  threshold: 5, // 5ms以上のブロッキングを検出
});

// express setup:
const express = require('express');
const app = express();

// view engine setup:
require("marko/node-require"); // Allow Node.js to require and load `.marko` files
const markoExpress = require("marko/express");
app.use(markoExpress()); //enable res.marko(template, data)

// 巨大なダミーデータ
const data = {
  "list": [
    { "list": ["ここに巨大な配列", ...] },
    { "list": ["ここに巨大な配列", ...] },
    ...
  ],
};

// GET /
app.get("/", function(req, res) {
  res.setHeader("content-type", "text/html");
  require("./views/index.marko").render(data, res); // ストリームに出力 (express の res.render() は使いません!)
});

app.listen(3000);

views/index.marko

<div>
  <for|item| of=input.list>
  <div>
    <for|item| of=item.list>
    <span>${item}</span>
    </for>
  </div>
  </for>
</div>

<div>
  <for|item| of=input.list>
  <div>
    <for|item| of=item.list>
    <span>${item}</span>
    </for>
  </div>
  </for>
</div>

<div>
  <for|item| of=input.list>
  <div>
    <for|item| of=item.list>
    <span>${item}</span>
    </for>
  </div>
  </for>
</div>

こちらも同じデータを無駄にループしてレンダリングしています。

server.jsを実行し http://localhost:3000/ へアクセスすると、サーバサイドのログに以下のように出力されました:

...
Blocked for 37.09147399997711ms, operation started here: [
  '    at Server.connectionListener (_http_server.js:396:3)',
  '    at Server.emit (events.js:311:20)',
  '    at TCP.onconnection (net.js:1554:8)'
]

ブロッキング時間 (marko): 37.09147399997711ms

こちらも何回か試してみましたが、だいたいコレぐらいの数値でした。 つまり、marko は ejs のおおよそ半分ぐらいしかブロッキングしないということです。素晴らしい!

ちなみに上記コードのrequire("./views/index.marko")でNode.jsのモジュールシステム自身が持つキャッシュ機構が利用されます。

ejs と marko の共存

ejs と marko を共存させるため、server.js のコードを以下のようにします:

server.js (ejs + marko)

// ブロッキング検出
const blocked = require('blocked-at');
blocked((time, stack) => {
  console.log(`Blocked for ${time}ms, operation started here:`, stack)
}, {
  threshold: 5, // 5ms以上のブロッキングを検出
});

// express:
const express = require('express');
const app = express();

// view engine setup: (marko)
require("marko/node-require"); // Allow Node.js to require and load `.marko` files
// view engine setup: (ejs)
const ejs = require('ejs'),
const LRU = require('lru-cache');
ejs.cache = new LRU(100); // キャッシュも効かせる
app.set('views', './views');
app.set('view engine', 'ejs');

// 巨大なダミーデータ
const data = {
  "list": [
    { "list": ["ここに巨大な配列", ...] },
    { "list": ["ここに巨大な配列", ...] },
    ...
  ],
};

// GET /ejs (ejsを使ったレンダリング)
app.get("/ejs", function(req, res) {
  res.render("index", data); // views/index.ejs を参照
});

// GET /marko (markoを使ったストリームなレンダリング)
app.get("/marko", function(req, res) {
  res.setHeader("content-type", "text/html");
  require(app.get("views") + "/index.marko").render(data, res);
});

app.listen(3000);

上記のコードは、 /ejs なページは ejs で描画し、 /marko なページは marko でストリーム描画するといったように、expressアプリケーション内で ejs と marko を共存させています。 これにより、軽いページは従来通り ejs にお任せし、巨大なページは marko のストリーム出力に任せるといった使い方ができます。

今回のやり方なら既存の ejs なページへの変更が不要なので、既存プロジェクトにも導入しやすいと思います。

以上です。


芽萌丸プログラミング部
芽萌丸プログラミング部 @programming
プログラミング関連アカウント。Web標準技術を中心に書いていきます。フロントエンドからサーバサイドまで JavaScript だけで済ませたい人たちの集いです。記事は主に @TanakaSoftwareLab が担当。