先日参加したインターンシップで Protocol Buffers を知ったので、実際に通信速度と容量の違いをJSON形式と比較してみました。
Protocol Buffers とは
Wikipediaで検索すると、構造データのシリアライズを目的とした技術スタック という風に出てくるのですが、これではあまり意味が分かりませんね。(笑)自分もあまりこちらに関してはわかっていません。
自分が使ってみて実際に恩恵を受けたのは、JSONより通信容量が大きく、速度が速いことです。フォーマットがバイナリのため、通信データ量が削減されます。それに伴って、通信速度も上がります。
制作物概要
今回は通信容量と速度を知りたいので、バックエンドでモックデータを作成し、それをフロントで呼び出すだけのものを作ります。
フロントエンドは Next.js、バックエンドは Railsで開発します。
開発
1. バックエンド
Ⅰ. リポジトリから、.protoファイルを拝借
送受信するデータのスキーマを定義したProtoファイルを取り込みます。もし、このサブモジュール(ある Git リポジトリの中の、別の Git リポジトリを含める仕組み)の中に、また別のサブモジュールが含まれている場合は、どちらのコマンドも実行します。
git submodule add https://github.com/yumachin/proto.git proto
git submodule update --init --recursive
Ⅱ. gem "google-protobuf" を追加 ⇒ bundle i
Ⅲ. protoc を使って、.protoファイルをRubyのクラスに変換
protoc --ruby_out=./app ./proto/Proto/*.proto
Ⅳ. Protocol Bufferでレスを返すコントローラーを定義
require_relative '../proto/Proto/task_pb'
class TasksController < ApplicationController
def index
# モックデータを100個ぐらい準備する
tasks = [
{ email: "test1@test.com", companyName: "テスト1株式会社", deadline: "X月XX日", task: "YYY", submitTo: "ZZZ" },
]
response = TaskResponse.new(tasks: tasks.map { |task| Task.new(task) })
# .to_proto: protobuf 形式に変換
render plain: response.to_proto, content_type: "application/x-protobuf"
end
end
Ⅴ. JSONでレスを返すコントローラーを定義
class BooksController < ApplicationController
def index
# モックデータを100個ぐらい準備する
books = [
{ email: "test2@test.com", companyName: "テスト2株式会社", deadline: "X月XX日", task: "YYY", submitTo: "ZZZ" },
]
render json: books
end
end
2. フロントエンド
Ⅰ. リポジトリから、.proto ファイルを拝借
バックエンドと同様です。
git submodule add https://github.com/yumachin/proto.git proto
git submodule update --init --recursive
Ⅱ. proto ファイルを JS や TS のコードに変換するためのツールをインストール
npm i protobufjs-cli
Ⅲ. src ディレクトリに、generated ディレクトリを作成
Ⅳ. proto ファイルを JS や TS のコードに変換
npx pbjs --no-verify --no-delimited -t static-module -w es6 -o ./src/generated/protocol.js ./proto/Proto/*.proto && npx pbts -o ./src/generated/protocol.d.ts ./src/generated/protocol.js
Ⅴ: page.tsx を記述
"use client";
import { useEffect, useState } from "react";
import { fetchBooks, fetchTasks } from "../utils/api";
import { ITask } from "@/generated/protocol";
// import { makeDummyTask } from "@/mock/mock";
export default function Home() {
// モックデータ作成
// const data = [ makeDummyTask(1), makeDummyTask(2), makeDummyTask(3)]
// ➀ protobuf
const [tasks, setTasks] = useState<ITask[]>([]);
useEffect(() => {
const getTasks = async () => {
const tasks = await fetchTasks();
setTasks(tasks);
};
getTasks();
}, []);
// ➁ JSON
const [books, setBooks] = useState<Book[]>([]);
useEffect(() => {
const getBooks = async () => {
const books = await fetchBooks();
setBooks(books);
};
getBooks();
}, []);
return (
<div>
<h1>Tasks</h1>
<ul>
{tasks.map((task: ITask, index: number) => (
<li key={index}>
{task.companyName} / {task.email} / {task.deadline} / {task.task} / {task.submitTo}
</li>
))}
</ul>
<h1>Books</h1>
<ul>
{books.map((book: Book, index: number) => (
<li key={index}>
{book.companyName} / {book.email} / {book.deadline} / {book.task} / {book.submitTo}
</li>
))}
</ul>
</div>
);
};
type Book = {
email: string;
companyName: string;
deadline: string;
task: string;
submitTo: string;
}
Ⅵ. api を叩く関数 を定義
import * as proto from "../generated/protocol";
export async function fetchTasks() {
console.time("Proto の fetch 時間");
const res = await fetch("http://localhost:3001/tasks", {
headers: { Accept: "application/x-protobuf" },
cache: "no-store"
});
// arrayBuffer(): バイナリデータとして取得
const buffer = await res.arrayBuffer();
// Uint8Array: protobufのデコード用に準備
const uint8Array = new Uint8Array(buffer);
const taskResponse = proto.TaskResponse.decode(uint8Array);
console.timeEnd("Proto の fetch 時間");
return taskResponse.tasks;
};
export async function fetchBooks() {
console.time("JSON の fetch 時間");
const res = await fetch("http://localhost:3001/books", {
cache: "no-store"
});
const data = await res.json();
console.timeEnd("JSON の fetch 時間");
return data;
};
結果
送信データがオブジェクトで量も少なかったので、通信速度はあまり変わりませんでしたが、通信容量は格段に小さくなりました。
今後のアプリ制作においては、UX やパフォーマンスを意識して作成していきたいです!