Help us understand the problem. What is going on with this article?

twigでバッファリング出力する。

More than 1 year has passed since last update.

概要

「twigで出力してるとTTFBが遅い」

PageSpeed Insights (Googleのサイトのスコア)出すためのアレ
https://developers.google.com/speed/docs/insights/Server

スコアのために小細工する。

前提

  1. 無名クラスとか使うためにphp7以上
  2. 出力中に例外発生とかでHTMLのelement treeが崩れるパターンはこの際、度外視する
  3. テンプレートエンジンに重きを置くこと自体の時代遅れ感は気にしない
composer.json
"require": {
    "php": ">=7.0.0",
    "twig/twig": "^2.0"
}

プログラムとテンプレート

  1. 変数sleepingを表示する
  2. 変数sleepingはオブジェクトなので、__toString () が呼び出される
  3. __toString () の処理中にsleep(1)する。
index.html.twig
{{ sleeping }}
1
{{ sleeping }}
2
{{ sleeping }}
3
{{ sleeping }}
4
{{ sleeping }}
5
index.php
echo $twig->render('index.html.twig', array('sleeping ' => new class {
    public function __toString () {
        sleep(1);
        return "-";
    }
}));

ここまででsleep(1)が5回呼び出されるようになる。
うん、おそいおそい。

コンパイルされたtwig出力結果

qawsedrtgyujikolp.php
// line 25
echo twig_escape_filter($this->env, (isset($context["sleeping"]) || array_key_exists("sleeping", $context) ? $context["sleeping"] : (function () { throw new Twig_Error_Runtime('Variable "sleeping" does not exist.', 25, $this->source); })()), "html", null, true);
echo "1";
// line 27
echo twig_escape_filter($this->env, (isset($context["sleeping"]) || array_key_exists("sleeping", $context) ? $context["sleeping"] : (function () { throw new Twig_Error_Runtime('Variable "sleeping" does not exist.', 27, $this->source); })()), "html", null, true);
echo "2";
// line 29
echo twig_escape_filter($this->env, (isset($context["sleeping"]) || array_key_exists("sleeping", $context) ? $context["sleeping"] : (function () { throw new Twig_Error_Runtime('Variable "sleeping" does not exist.', 29, $this->source); })()), "html", null, true);
echo "3";
// line 31
echo twig_escape_filter($this->env, (isset($context["sleeping"]) || array_key_exists("sleeping", $context) ? $context["sleeping"] : (function () { throw new Twig_Error_Runtime('Variable "sleeping" does not exist.', 31, $this->source); })()), "html", null, true);
echo "4";
// line 33
echo twig_escape_filter($this->env, (isset($context["sleeping"]) || array_key_exists("sleeping", $context) ? $context["sleeping"] : (function () { throw new Twig_Error_Runtime('Variable "sleeping" does not exist.', 33, $this->source); })()), "html", null, true);
echo "5";

ちょっと見やすくしてます。
めちゃechoしてる。

バッファリング出力するには

  1. 開始時点でバッファリングを宣言する。
  2. 出力のつどバッファを出力する

この二つをクリアする。

結果

index.php
// コンパイラに細工
// compileの前にバッファリングを宣言。
// addDebugInfoでバッファを吐き出す(line {$n}が書かれるタイミング)
$twig->setCompiler(new class($twig) extends Twig_Compiler {
    private $lastLine;
    public function compile(Twig_Node $node, $indentation = 0)
    {
        mb_http_output("UTF-8");
        echo str_pad("\n ",4096);
        ob_end_flush();
        ob_start('mb_output_handler');

        $this->lastLine = null;
        return parent::compile($node, $indentation);
    }
    public function addDebugInfo(Twig_Node $node)
    {
        if ($node->getTemplateLine() != $this->lastLine) {
            $this->write("ob_flush();");
            $this->write("flush();");
            $this->lastLine = $node->getTemplateLine();
        }
        return parent::addDebugInfo($node);
    }
});
// まとめてレンダリング(echo $twig->render)から
// レンダリングしながら表示($twig->display)に変更
$twig->display('index.html.twig', array('sleeping ' => new class {
    public function __toString () {
        sleep(1);
        return "-";
    }
}));

はい。この結果以下が出てきます。
コンパイル前にバッファリングの宣言部も処理されます。

qawsedrtgyujikolp.php
// line 25
echo twig_escape_filter($this->env, (isset($context["sleeping"]) || array_key_exists("sleeping", $context) ? $context["sleeping"] : (function () { throw new Twig_Error_Runtime('Variable "sleeping" does not exist.', 25, $this->source); })()), "html", null, true);
echo "1";
ob_flush();
flush();
// line 27
echo twig_escape_filter($this->env, (isset($context["sleeping"]) || array_key_exists("sleeping", $context) ? $context["sleeping"] : (function () { throw new Twig_Error_Runtime('Variable "sleeping" does not exist.', 27, $this->source); })()), "html", null, true);
echo "2";
ob_flush();
flush();
// line 29
echo twig_escape_filter($this->env, (isset($context["sleeping"]) || array_key_exists("sleeping", $context) ? $context["sleeping"] : (function () { throw new Twig_Error_Runtime('Variable "sleeping" does not exist.', 29, $this->source); })()), "html", null, true);
echo "3";
ob_flush();
flush();
// line 31
echo twig_escape_filter($this->env, (isset($context["sleeping"]) || array_key_exists("sleeping", $context) ? $context["sleeping"] : (function () { throw new Twig_Error_Runtime('Variable "sleeping" does not exist.', 31, $this->source); })()), "html", null, true);
echo "4";
ob_flush();
flush();
// line 33
echo twig_escape_filter($this->env, (isset($context["sleeping"]) || array_key_exists("sleeping", $context) ? $context["sleeping"] : (function () { throw new Twig_Error_Runtime('Variable "sleeping" does not exist.', 33, $this->source); })()), "html", null, true);
echo "5";
ob_flush();
flush();

課題

1、前提に書いてた例外パターン。

2.出力中に例外発生とかでHTMLのelement treeが崩れるパターンはこの際、度外視する

最悪、出力した内容に合わせて閉じタグ入れてけばいいのだけど。
そのために出力を評価しないといけないのでどこかに出力内容が必要になる。
ブラウザの補完に任せて放置でもいいけど、ちゃんとするには結局でNodeに手を入れないといけない。

addDebugInfoって、処理差し込んでもいいの?

debug:falseでもいけたからとりあえずこうしたけど、、、
どうしてもぬぐえない筋悪感(すじわるかん)

追記

packagist/githubに乗せました。

https://packagist.org/packages/mitukiti11/twig_ob_compiler
https://github.com/mitukiti11/twig_ob_compiler

追記2

コンパイラにしている細工がいろいろとひどいことが分かった。

最新のgithubでは修正済み。
twigのdisplayはもとからob_startしていて。フラッシュ関数を挟み込むだけで出力されること。
コンパイルしたphpファイルでob_startせず、コンパイル中にob_startしていたとかetc...

github/composerに乗せるにあたりtravis.ciとかユニットテスト/カバレジを気にし始めて突き詰めるといろいろわかりますね。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away