WebView+Canvas vs ネイティブ パフォーマンス比較 iOS編

More than 1 year has passed since last update.

WebView+Canvas+JavaScriptとネイティブのパフォーマンスを比較するために、実験を行ってみました。
非常に大雑把な比較になりますので、その点はご容赦ください。
Simulator Screen Shot 2016.02.29 11.37.23.png

検証方法
→色のついた正方形を複数画面内でランダムな方向に移動させる。WebViewとネイティブでそれぞれ正方形の数を変化させて、パフォーマンスの推移を測定する。

検証環境
→Xcode7.2.1 iPhone6 Plus iOS9.2.1

WebView+Canvas+JavaScript側の実装
→CanvasとJavaScriptのロジックを含むindex.htmlをバンドルし、UIWebViewで表示させる。20ミリ秒間隔で、fillRect関数を用いて複数の正方形を描画する。WKWebViewは原因不明の理由によりロジックがうまく動作せず、またパフォーマンスが表示されないため用いない。

ネイティブ側の実装
→UIViewで複数の青い正方形を作りself.viewにaddSubViewする。20ミリ秒間隔で、正方形の座標を変更する。

結果
スクリーンショット 2016-02-29 11.05.36.png
Nは正方形の数です。
スクリーンショット 2016-02-29 11.05.51.png
スクリーンショット 2016-02-29 11.06.01.png
予想通りネイティブの方がパフォーマンスは上ですが、WebViewの方がどうしようもないほどパフォーマンスが劣っているわけではなさそうです。
なお、ネイティブの方はN数が大きくなるとCPUの使用量が頭打ちになりますが、その分fpsを落として対応しているようです。WebViewの方は、N数が増えても確認できる範囲ではCPUの消費量は頭打ちになりませんでした。そのため、N数が多いとiPhoneが発熱するので少々危険かもしれません。

画像を用いたり、色数を増やしたりすると結果が異なってくるとは思うのですが、大雑把にWebView+Canvas+JavaScriptとネイティブのパフォーマンス差が把握できたかと思います。
Webの普遍性を優先するのか、ネイティブのパフォーマンスを優先するのか判断する際の一助になればと思います。

Androidでも同様の実験を行ってみたいですが、機種差が激しそうですね。

以下に、今回用いたコードを記述します。
→WebView + Canvas + JavaScript

index.html
<!DOCTYPE html>

<html>
<head>
  <title>-Experiment-</title>
</head>

<body style="padding:0px;margin:0px;">
  <canvas id="main_canvas"></canvas>
  <script type="text/javascript">

  var canvas = document.getElementById("main_canvas");
  canvas.style.width = window.innerWidth + 'px';
  canvas.style.height = window.innerHeight + 'px';
  canvas.style.margin = "0px";
  canvas.style.transform = "translate3d(0, 0, 0)";
  canvas.width = window.innerWidth * window.devicePixelRatio;
  canvas.height = window.innerHeight * window.devicePixelRatio;
  var context = canvas.getContext("2d");

  var balls = [];
  var speed = canvas.width * 0.002;
  var size = canvas.width * 0.05;
  for (var i=0; i<1000; i++) {
    var angle = Math.PI*2*Math.random();
    var ball = {
      size:size,
      position:{x:canvas.width/2, y:canvas.height/2},
      speed:{x:speed*Math.cos(angle), y:speed*Math.sin(angle)},
      border:{left:size/2, right:canvas.width-size/2, top:size/2, bottom:canvas.height-size/2},
    };
    balls.push(ball);
  }

  context.fillStyle = "#0000ff";

  var timer = setInterval(function(){

    context.clearRect(0, 0, canvas.width, canvas.height);

    for (var i=0; i<balls.length; i++){
      var ball = balls[i];
      ball.position.x += ball.speed.x;
      if (ball.position.x < ball.border.left) {
        ball.position.x = ball.border.left;
        ball.speed.x = Math.abs(ball.speed.x);
      };
      if (ball.position.x > ball.border.right) {
        ball.position.x = ball.border.right;
        ball.speed.x = -Math.abs(ball.speed.x);
      };
      ball.position.y += ball.speed.y;
      if (ball.position.y < ball.border.top) {
        ball.position.y = ball.border.top;
        ball.speed.y = Math.abs(ball.speed.y);
      };
      if (ball.position.y > ball.border.bottom) {
        ball.position.y = ball.border.bottom;
        ball.speed.y = -Math.abs(ball.speed.y);
      };

      context.fillRect(
        Math.round(ball.position.x-ball.size/2),
        Math.round(ball.position.y-ball.size/2),
        Math.round(ball.size),
        Math.round(ball.size)
      );
    }
  } , 20);

</script>
</body>
</html>
ViewController.swift
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        let url = NSBundle.mainBundle().pathForResource("index", ofType: "html");
        let reqURL = NSURL(string: url!)
        let req = NSURLRequest(URL: reqURL!)
        let webView = UIWebView(frame: CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height))
        webView.backgroundColor = UIColor.yellowColor()
        self.view.addSubview(webView)
        webView.loadRequest(req)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

→ネイティブ

ViewController.swift
import UIKit

class ViewController: UIViewController {

    var balls:[Ball] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        let speed = self.view.frame.size.width * 0.002
        let size = self.view.frame.size.width * 0.05
        for var i=0; i<1000; ++i{
            let ball = Ball()
            ball.frame = CGRectMake(0, 0, size, size)
            ball.center = self.view.center
            let rand = CGFloat(arc4random_uniform(UINT32_MAX)) / CGFloat(UINT32_MAX)
            let angle = CGFloat(M_PI*2)*rand
            ball.speed = CGVectorMake(speed*cos(angle), speed*sin(angle))
            ball.border = Border(
                left:size/2,
                right:self.view.frame.size.width-size/2,
                top:size/2,
                bottom:self.view.frame.size.height-size/2
            )
            ball.backgroundColor = UIColor.blueColor()
            self.view.addSubview(ball)
            balls.append(ball)
        }

        NSTimer.scheduledTimerWithTimeInterval(
            0.02,
            target: self,
            selector: "doFrame",
            userInfo: nil,
            repeats: true
        )
    }

    func doFrame(){
        for var i=0; i<balls.count; ++i {
            let ball = balls[i]
            ball.center.x += ball.speed!.dx
            if ball.center.x < ball.border?.left{
                ball.center.x = ball.border!.left!
                ball.speed?.dx = abs(ball.speed!.dx)
            }
            if ball.center.x > ball.border?.right{
                ball.center.x = ball.border!.right!
                ball.speed?.dx = -abs(ball.speed!.dx)
            }
            ball.center.y += ball.speed!.dy
            if ball.center.y < ball.border?.top{
                ball.center.y = ball.border!.top!
                ball.speed?.dy = abs(ball.speed!.dy)
            }
            if ball.center.y > ball.border?.bottom{
                ball.center.y = ball.border!.bottom!
                ball.speed?.dy = -abs(ball.speed!.dy)
            }
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}
Ball.swift
import UIKit

class Ball: UIView {
    var speed:CGVector?
    var border:Border?
}

struct Border {
    var left:CGFloat?
    var right:CGFloat?
    var top:CGFloat?
    var bottom:CGFloat?
}