- 技術 💻
PWAの作り方|Service Worker最小コードとキャッシュ戦略一覧
PWA(プログレッシブウェブアプリ)とは、Webサイトをアプリのようにホーム画面やデスクトップへインストールでき、オフラインでも動かせるようにする仕組みです。 必要なのは、アプリ名やアイコンを書いた Web App Manifest(manifest.webmanifest) と、通信の間に入ってキャッシュを扱う Service Worker(sw.js)、そして HTTPS(開発中は localhost)の3つです。
この記事では、そのままコピペで動く最小構成一式を載せ、Playwright(ブラウザ自動操作ツール)と Chromium で、SW の登録・キャッシュの中身・サーバーを止めた状態での表示・新バージョンへの切り替えまで実際に検証した結果を表にしました。5つのキャッシュ戦略の比較表とコード、更新の流れ(skipWaiting・clients.claim・古いキャッシュ削除・更新通知)、検証中に見つかったつまずきどころもまとめています。
PWAの作り方の早見表
| やりたいこと | 書く場所 | 最小コード・設定 |
|---|---|---|
| インストール可能にする | <head> |
<link rel="manifest" href="/manifest.webmanifest"> |
| アプリ名 | manifest | name または short_name |
| アイコン | manifest | icons に 192px と 512px |
| 起動URL・表示形式 | manifest | start_url と display: "standalone" |
| SW を登録 | ページのJS | navigator.serviceWorker.register("/sw.js") |
| 先にキャッシュ | sw.js install |
cache.addAll([...]) |
| 古いキャッシュ削除 | sw.js activate |
caches.keys() → caches.delete() |
| すぐ制御を開始 | sw.js activate |
self.clients.claim() |
| 通信を横取り | sw.js fetch |
event.respondWith(...) |
| 新しい SW をすぐ有効化 | sw.js | self.skipWaiting()(ボタンで実行が安全) |
| 更新を知らせる | ページのJS | updatefound → installed で通知表示 |
| 配信条件 | サーバー | HTTPS、または localhost / 127.0.0.1 |
| 確認する | Chrome DevTools | Application パネルの Manifest / Service workers / Cache storage |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「PWAの作り方|Service Worker最小コードとキャッシュ戦略一覧」
PWAの要件(MDN・web.devで確認)
PWA としてインストールできる条件は、ブラウザごとに少し異なります。 2026年10月時点で MDN「Making PWAs installable」と web.dev「What does it take to be installable?」に書かれている内容をまとめます。
| 要件 | 内容 | 出典 |
|---|---|---|
| manifest の読み込み | すべてのページで <link rel="manifest"> |
MDN |
| 名前 | name または short_name |
MDN・web.dev |
| アイコン | 192px と 512px を含む | MDN・web.dev |
| 起動URL | start_url |
MDN・web.dev |
| 表示形式 | display(fullscreen / standalone / minimal-ui / window-controls-overlay)または display_override |
MDN・web.dev |
| ネイティブアプリ優先指定 | prefer_related_applications がない、または false |
MDN・web.dev |
| 通信 | HTTPS、または localhost / 127.0.0.1 |
MDN |
| ユーザー操作(Chrome) | ページを1回以上クリック/タップし、30秒以上閲覧 | web.dev |
| Service Worker | インストールの必須要件ではない(オフライン対応に使う) | MDN |
Service Worker はインストールの条件からは外れていますが、オフライン表示や高速化には欠かせません。 この記事では両方を入れた構成で進めます。
最小構成のファイル一式(コピペ用)
次の5種類のファイルを同じフォルダに置けば、インストール可能でオフラインでも開ける PWA になります。
site/
├── index.html
├── manifest.webmanifest
├── sw.js
├── app.js
├── style.css
├── data/news.json
└── icons/
├── icon-192.png
└── icon-512.png
index.html
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>タスクメモ</title>
<meta name="theme-color" content="#2563eb">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="icon" href="/icons/icon-192.png">
<link rel="apple-touch-icon" href="/icons/icon-192.png">
<link rel="stylesheet" href="/style.css">
</head>
<body>
<h1>タスクメモ</h1>
<p id="status">オンライン</p>
<ul id="news"></ul>
<div id="update" hidden>
新しいバージョンがあります。
<button id="reload" type="button">更新する</button>
</div>
<script src="/app.js" defer></script>
</body>
</html>
manifest.webmanifest
拡張子は .webmanifest が仕様上の推奨で、manifest.json という名前でも動きます。 どちらでも <link rel="manifest"> の href と一致させれば問題ありません。
{
"name": "タスクメモ",
"short_name": "タスク",
"id": "/",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2563eb",
"lang": "ja",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}
| メンバー | 意味 |
|---|---|
name / short_name |
正式名/ホーム画面など狭い場所での名前 |
id |
アプリの識別子(start_url を変えても同じアプリとして扱われる) |
start_url / scope |
起動時に開くURL/アプリとして扱う範囲 |
display |
standalone でアドレスバーなしの独立ウィンドウ |
background_color / theme_color |
起動画面の背景色/タイトルバーの色 |
purpose: "maskable" |
丸型などに切り抜かれる前提のアイコン |
sw.js(Service Worker 本体)
// バージョンを上げると新しいキャッシュが作られ、古いものは activate で削除される
const VERSION = "v1";
const CACHE = `app-${VERSION}`;
const PRECACHE = [
"/",
"/index.html",
"/style.css",
"/app.js",
"/manifest.webmanifest",
"/icons/icon-192.png",
"/icons/icon-512.png",
];
// install:必要なファイルを先にキャッシュ
self.addEventListener("install", (event) => {
event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(PRECACHE)));
});
// activate:古いキャッシュを削除し、開いているページをすぐ制御
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
const keys = await caches.keys();
await Promise.all(keys.filter((k) => k.startsWith("app-") && k !== CACHE).map((k) => caches.delete(k)));
await self.clients.claim();
})()
);
});
// ページから「更新する」が押されたら待機をやめて有効化
self.addEventListener("message", (event) => {
if (event.data?.type === "SKIP_WAITING") self.skipWaiting();
});
// ---- キャッシュ戦略 ----
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached;
const res = await fetch(request);
if (res.ok) (await caches.open(CACHE)).put(request, res.clone());
return res;
}
async function networkFirst(request) {
const cache = await caches.open(CACHE);
try {
const res = await fetch(request);
if (res.ok) cache.put(request, res.clone());
return res;
} catch (err) {
return (await cache.match(request)) || (await cache.match("/index.html")) || Response.error();
}
}
async function staleWhileRevalidate(event) {
const cache = await caches.open(CACHE);
const cached = await cache.match(event.request);
const network = fetch(event.request)
.then((res) => {
if (res.ok) cache.put(event.request, res.clone());
return res;
})
.catch(() => undefined);
event.waitUntil(network); // 裏側の更新を最後まで行わせる
return cached || (await network) || Response.error();
}
function networkOnly(request) {
return fetch(request);
}
async function cacheOnly(request) {
return (await caches.match(request)) || new Response("Not cached", { status: 504 });
}
// ---- 振り分け ----
self.addEventListener("fetch", (event) => {
const { request } = event;
const url = new URL(request.url);
if (request.method !== "GET" || url.origin !== location.origin) return;
if (request.mode === "navigate") {
event.respondWith(networkFirst(request)); // HTML:最新優先
} else if (url.pathname.startsWith("/data/")) {
event.respondWith(staleWhileRevalidate(event)); // JSON:すぐ表示+裏で更新
} else if (url.pathname.startsWith("/live/")) {
event.respondWith(networkOnly(request)); // 常に最新が必要
} else if (url.pathname.startsWith("/fixed/")) {
event.respondWith(cacheOnly(request)); // キャッシュ済みのみ
} else {
event.respondWith(cacheFirst(request)); // CSS・JS・画像
}
});
res.clone() が必要なのは、レスポンスの本文は1回しか読めないためです。 キャッシュ用とページへ返す用に複製しています。POST や別オリジン(外部CDNなど)のリクエストは return で素通りさせています。
app.js(登録・更新通知・オンライン表示)
// オンライン/オフライン表示
const statusEl = document.getElementById("status");
const showStatus = () => { statusEl.textContent = navigator.onLine ? "オンライン" : "オフライン"; };
addEventListener("online", showStatus);
addEventListener("offline", showStatus);
showStatus();
// お知らせを読み込む(Stale-While-Revalidate で配信)
fetch("/data/news.json")
.then((res) => res.json())
.then((data) => {
document.getElementById("news").innerHTML = data.items.map((t) => `<li>${t}</li>`).join("");
})
.catch(() => {});
// Service Worker の登録と更新通知
if ("serviceWorker" in navigator) {
window.addEventListener("load", async () => {
const reg = await navigator.serviceWorker.register("/sw.js");
const showUpdate = (worker) => {
const box = document.getElementById("update");
box.hidden = false;
document.getElementById("reload").onclick = () => worker.postMessage({ type: "SKIP_WAITING" });
};
// すでに待機中の新しい SW がある
if (reg.waiting && navigator.serviceWorker.controller) showUpdate(reg.waiting);
// 新しい SW が見つかった
reg.addEventListener("updatefound", () => {
const newWorker = reg.installing;
newWorker.addEventListener("statechange", () => {
if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
showUpdate(newWorker);
}
});
});
});
// 新しい SW に切り替わったら1回だけ再読み込み
// (初回の clients.claim() でも発火するため、元々制御されていた場合だけ)
const hadController = !!navigator.serviceWorker.controller;
let refreshing = false;
navigator.serviceWorker.addEventListener("controllerchange", () => {
if (!hadController || refreshing) return;
refreshing = true;
location.reload();
});
}
style.css と data/news.json
body { font-family: system-ui, sans-serif; margin: 2rem; }
#update { position: fixed; bottom: 1rem; right: 1rem; padding: 1rem; background: #fef3c7; }
{ "items": ["お知らせ v1"] }
アイコンを作る(Python 標準ライブラリのみ)
手元に画像がなくても、次のスクリプトで単色の 192px・512px アイコンを作れます。 site フォルダで実行します。
# make_icons.py:アイコン(192px・512px)を標準ライブラリだけで作る
import struct, zlib
def png(size, path, rgb=(37, 99, 235)):
row = b"\x00" + bytes(rgb) * size # 各行の先頭はフィルタ種別 0
raw = row * size
def chunk(tag, data):
c = tag + data
return struct.pack(">I", len(data)) + c + struct.pack(">I", zlib.crc32(c))
ihdr = struct.pack(">IIBBBBB", size, size, 8, 2, 0, 0, 0)
with open(path, "wb") as f:
f.write(b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr)
+ chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b""))
png(192, "icons/icon-192.png")
png(512, "icons/icon-512.png")
print("ok")
$ mkdir -p icons && python3 make_icons.py
ok
$ file icons/*
icons/icon-192.png: PNG image data, 192 x 192, 8-bit/color RGB, non-interlaced
icons/icon-512.png: PNG image data, 512 x 512, 8-bit/color RGB, non-interlaced
ローカルで配信する
Service Worker は HTTPS か localhost でしか登録できないため、file:// で開くのではなくローカルサーバーで配信します。 python3 -m http.server 8000 --directory site でも動きますが、検証では「毎回サーバーに確認させる」ヘッダーを付けた次のサーバーを使いました。ブラウザの HTTP キャッシュと SW のキャッシュが混ざらず、挙動が読みやすくなります。
# serve.py:Cache-Control: no-cache を付けて site フォルダを配信
import functools, http.server, sys
class Handler(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header("Cache-Control", "no-cache")
super().end_headers()
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
handler = functools.partial(Handler, directory="site")
http.server.ThreadingHTTPServer(("127.0.0.1", port), handler).serve_forever()
python3 serve.py 8000
# → http://127.0.0.1:8000/ を開く
Python の http.server は .webmanifest を Content-type: application/manifest+json で返すことも確認しました。
Playwrightで検証した結果
上の一式を Playwright 1.56 + Chromium で開き、登録からオフライン表示、新バージョンへの切り替えまでを自動で確かめました。 Chrome DevTools Protocol の Page.getAppManifest と Page.getInstallabilityErrors でインストール可否も確認しています。
登録とキャッシュ
| 確認項目 | 結果 |
|---|---|
navigator.serviceWorker.ready の scope |
http://127.0.0.1:7831/ |
初回表示直後の controller |
false(まだ制御されていない) |
clients.claim() 後の controller |
true |
caches.keys()(初回) |
["app-v1"]、中身は PRECACHE の7件 |
| 再読み込み後のキャッシュ | 上記+/data/news.json(SWR で追加) |
| manifest の解析エラー | [](なし) |
| インストール可否のエラー | [](なし=インストール可能) |
初回表示で発行された /data/news.json のリクエストはキャッシュされませんでした。 SW の登録より先に送られたリクエストは、clients.claim() で制御が始まってもさかのぼって横取りされないためです。2回目の表示から SW 経由になり、キャッシュに入りました。
サーバーを止めた状態(オフライン)
サーバーを停止し、context.setOffline(true) にしてから再読み込みした結果です。
| リクエスト | 戦略 | 結果 |
|---|---|---|
ページ(/) |
Network First | 表示できた(見出し「タスクメモ」、状態「オフライン」) |
/data/news.json |
Stale-While-Revalidate | 200(キャッシュから「お知らせ v1」) |
/style.css |
Cache First | 200 |
/live/now.json |
Network Only | TypeError: Failed to fetch |
/fixed/none.json(未キャッシュ) |
Cache Only | 504(SW が返した「Not cached」) |
Playwright 1.56 では context.setOffline(true) だけだと、SW 内の fetch() はネットワークに届き 200 が返りました。 オフラインの検証では、サーバーの停止も組み合わせると確実です。
戦略ごとの「新しい内容が届くタイミング」
オンラインに戻し、サーバー側のファイルを書き換えてから2回取得しました。
| ファイル | 戦略 | 1回目 | 2回目 |
|---|---|---|---|
news.json を v1 → v2 に変更 |
Stale-While-Revalidate | v1(キャッシュ) | v2(裏で更新済み) |
style.css に1行追加 |
Cache First | 変更前 | 変更前(SW 更新まで変わらない) |
live/now.json |
Network Only | 常にサーバーの内容 | 常にサーバーの内容 |
新バージョンへの切り替え
sw.js の VERSION を v2 に書き換え、registration.update() を呼んだ結果です。
| 段階 | active | waiting | caches.keys() |
画面 |
|---|---|---|---|---|
| 更新検出直後 | activated(v1) | installed(v2) | ["app-v1","app-v2"] |
「新しいバージョンがあります」表示 |
| 「更新する」クリック後 | activated(v2) | なし | ["app-v2"] |
自動で1回再読み込み |
skipWaiting() を押すまでは古い SW が動き続け、押すと activate で古いキャッシュ app-v1 が削除されました。
キャッシュ戦略の比較表とコードの選び方
| 戦略 | 動き | 速さ | 新しさ | オフライン | 向いているもの |
|---|---|---|---|---|---|
| Cache First | キャッシュ → なければネットワーク | 最速 | 低い(SW更新まで固定) | 可 | CSS・JS・画像・フォント(ファイル名にハッシュ付き) |
| Network First | ネットワーク → 失敗したらキャッシュ | 回線しだい | 高い | 可 | HTML、更新の多いページ |
| Stale-While-Revalidate | キャッシュを即返し、裏でネットワーク更新 | 速い | 次回から新しい | 可 | お知らせ一覧、アバター、設定JSON |
| Network Only | 常にネットワーク | 回線しだい | 最新 | 不可 | ログイン・注文確定などの API、POST |
| Cache Only | キャッシュのみ | 最速 | install 時点 | 可 | install で必ず入れた固定ファイル |
迷ったら「HTML は Network First、静的ファイルは Cache First、ちょっと古くてもよいデータは Stale-While-Revalidate」から始めると、表示の速さと新しさのバランスが取れます。各戦略の関数は上の sw.js にすべて入っているので、fetch の振り分けだけ変えれば使い分けられます。
更新の流れとライフサイクル図
新しい sw.js は「install → waiting(待機)→ activate」の順で切り替わり、待機中は古い SW がページを制御し続けます。 ブラウザはページを開いたときなどに sw.js を取得し、1バイトでも違えば新しい SW として install します。
stateDiagram-v2
[*] --> installing: register() / sw.js の変更を検出
installing --> installed: install 完了(cache.addAll)
installing --> redundant: install 失敗
installed --> activating: 古い SW の制御ページが0になる、または skipWaiting()
activating --> activated: activate 完了(古いキャッシュ削除・clients.claim)
activated --> redundant: さらに新しい SW に置き換わる
note right of installed
waiting 状態
ページで更新通知を出す
end note
| 処理 | どこで | 役割 | 書かないと |
|---|---|---|---|
event.waitUntil(cache.addAll()) |
install | 必要ファイルを揃えてから完了 | 一部のファイルが欠けた状態で有効化される |
self.skipWaiting() |
install または message | 待機を飛ばして有効化 | タブをすべて閉じるまで新版に切り替わらない |
| 古いキャッシュの削除 | activate | caches.keys() で旧版を消す |
旧版のキャッシュが残り続ける |
self.clients.claim() |
activate | 開いているページをすぐ制御 | 初回は次の再読み込みまで SW が効かない |
controllerchange で再読み込み |
ページ | 新しい SW の内容で表示し直す | 古いHTMLと新しいキャッシュが混ざる |
install の中でいきなり skipWaiting() すると、開いているページが古いHTMLのまま新しい SW に切り替わります。 この記事の構成のように「更新があります → ボタンで SKIP_WAITING」の流れにすると、利用者が切り替えのタイミングを選べます。
検証で見つかったつまずきどころ
controllerchange は、初回の clients.claim() でも発火します。 最初は「controllerchange で無条件に location.reload()」と書いていたため、初めて開いた直後にページが勝手に再読み込みされ、Playwright で次のエラーになりました。
page.evaluate: Execution context was destroyed, most likely because of a navigation
直す前と後は次のとおりです。
// 直す前:初回の claim でも再読み込みされる
navigator.serviceWorker.addEventListener("controllerchange", () => location.reload());
// 直した後:元々 SW に制御されていたページだけ再読み込み
const hadController = !!navigator.serviceWorker.controller;
navigator.serviceWorker.addEventListener("controllerchange", () => {
if (!hadController) return;
location.reload();
});
DevToolsでの確認手順
Chrome の DevTools(F12)の Application パネルで、manifest・SW・キャッシュをまとめて確認できます。
| 確認したいこと | 場所 | 見るポイント |
|---|---|---|
| manifest が読めているか | Application → Manifest | 名前・アイコン・エラー表示 |
| SW の状態 | Application → Service workers | Status(activated / waiting)、Source |
| 待機中の SW を有効化 | 同上 → skipWaiting リンク | 更新通知を出さずに切り替えたいとき |
| 毎回 SW を更新 | 同上 → Update on reload | 開発中はオンにすると便利 |
| オフライン確認 | 同上 → Offline、または Network の throttling | ページがキャッシュから表示されるか |
| キャッシュの中身 | Application → Cache storage | app-v1 などのキャッシュ名と URL |
| まとめて削除 | Application → Storage → Clear site data | 初めて開いた状態に戻す |
| インストール | アドレスバー右のインストールアイコン | 条件を満たすと表示 |
Workboxの概要
Workbox は、Google が提供する Service Worker 用のライブラリで、上の5つの戦略が部品として用意されています。 2026年10月時点の npm の最新版は workbox-build 7.4.1 です。プリキャッシュの一覧(ファイル名とハッシュ)をビルド時に自動生成できるため、ファイルが多いサイトでは手書きより管理が楽になります。
手書きの cacheFirst などは、Workbox ではそれぞれ CacheFirst NetworkFirst StaleWhileRevalidate NetworkOnly CacheOnly クラスに対応します。
// Workbox を使った振り分けの例(ビルドツールで sw.js にまとめる前提)
import { registerRoute } from "workbox-routing";
import { NetworkFirst, StaleWhileRevalidate, CacheFirst } from "workbox-strategies";
registerRoute(({ request }) => request.mode === "navigate", new NetworkFirst());
registerRoute(({ url }) => url.pathname.startsWith("/data/"), new StaleWhileRevalidate());
registerRoute(({ request }) => ["style", "script", "image"].includes(request.destination), new CacheFirst());
※ Workbox のコードはこの記事では実行していません(書き方の対応を示すための例です)。Vite を使う場合は vite-plugin-pwa(npm 最新 1.3.0)から Workbox を利用する方法もあります。
iOS(iPhone・iPad)での注意点
iOS でもホーム画面に追加した PWA は動きますが、Android の Chrome とは条件が異なります。 公式情報(MDN、WebKit ブログ)で確認できた範囲をまとめます。
| 項目 | 内容 | 出典 |
|---|---|---|
| ホーム画面への追加 | Safari に加え、iOS 16.4 以降は条件を満たす他のブラウザからも追加できる | MDN・WebKit |
| Web Push 通知 | iOS / iPadOS 16.4 以降、ホーム画面に追加した Web アプリだけが対象 | WebKit |
| Web Push の条件 | manifest の display が standalone または fullscreen |
WebKit |
| 通知の許可 | 「購読する」ボタンのタップなど、利用者の操作をきっかけに許可を求める | WebKit |
| アイコンバッジ | Badging API に対応(16.4 以降) | WebKit |
通知の許可をページ表示と同時に求めず、ボタン操作のあとに求める設計にしておくと、iOS と他のブラウザの両方で動きます。
よくある質問
Q. manifest.json と manifest.webmanifest のどちらにすべきですか?
A. どちらでも動きます。 仕様上の推奨拡張子は .webmanifest で、MIME タイプは application/manifest+json です。大切なのは <link rel="manifest" href="..."> とファイル名を一致させることです。
Q. sw.js はどこに置けばよいですか?
A. サイトのルート(/sw.js)に置くのが基本です。 SW が制御できる範囲(scope)は、既定では sw.js があるフォルダ以下になります。/js/sw.js に置くと /js/ 以下しか制御できません。
Q. sw.js を更新したのに反映されません。
A. 新しい SW は waiting で止まっています。タブをすべて閉じる、skipWaiting() を呼ぶ、DevTools の「skipWaiting」を押すのいずれかで切り替わります。開発中は「Update on reload」をオンにすると便利です。
Q. ログインが必要なページや API もキャッシュしてよいですか? A. 利用者ごとに内容が変わる API は Network Only にし、キャッシュに入れないのが安全です。共有端末で別の人のデータが表示されることを防げます。
まとめ
- PWA は manifest + Service Worker + HTTPS(開発は localhost)。manifest には
name、192px・512px のicons、start_url、displayを書く - 最小構成5ファイルで、インストール可否エラー0件・サーバー停止中も表示できることを Playwright で確認
- キャッシュ戦略は5種類。HTML は Network First、静的ファイルは Cache First、多少古くてよいデータは Stale-While-Revalidate が出発点
- 更新は install → waiting → activate。
skipWaiting()をボタンで実行し、activate で古いキャッシュを削除、clients.claim()で即制御 controllerchangeは初回の claim でも発火するので、元々制御されていたときだけ再読み込みする- 確認は DevTools の Application パネル、ファイルが多ければ Workbox で戦略を部品化
- iOS の Web Push はホーム画面に追加したアプリのみ・利用者の操作で許可
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「PWAの作り方|Service Worker最小コードとキャッシュ戦略一覧」
株式会社RJCでは、PWA と Service Worker の仕組みを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。