JavaScript
Xcode
iOS
canvas
Swift

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

More than 3 years have 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?
}