Apex

JavaScript RemotingとApexの非同期処理の違い

ボタンを押した瞬間に画面をロックせず、サーバー側の処理結果だけを画面に反映する構成を作ります。JavaScript Remotingが実際には何を非同期にしているのか、@future・Queueable・Batch Apexとの違い、ジョブの進捗をAsyncApexJobで照会する方法まで説明します。

2023.03.01

この記事で扱う非同期の意味

「非同期処理」という言葉には2つの意味が混ざりがちです。1つはブラウザから見た非同期で、ページを再読み込みせずにサーバーへリクエストを送る仕組みです。もう1つはApexの実行基盤から見た非同期で、リクエストを送った処理とは別のトランザクションでジョブとして動く@future・Queueable Apex・Batch Apexを指します。

この記事のコードは前者です。Visualforce.remoting.Manager.invokeActionというJavaScript Remotingを使い、ボタンを押した後も画面をロックせずに取引先の一覧を取得し、その結果で取引先を更新します。まずこの構成そのものを見て、後半で後者との違いを整理します。

Visualforceページ

<apex:page controller="VisualforcePageAsyncProcessController"
    apiVersion="67.0"
    sidebar="false"
    showHeader="false"
    standardStylesheets="false"
    applyHtmlTag="false"
    applyBodyTag="false"
    docType="html-5.0"
    >
    <html>
    <body>
        <div id="info">Visualforceページ非同期処理</div>
        <br/>
        <div id="Size"></div>
        <div id="State"></div>
        <script type="module">
            document.getElementById("Size").innerText = "件数:取得中";
            document.getElementById("State").innerText = "ステータス:開始";
            Visualforce.remoting.timeout = 120000;
            var executedRecords = '';
            var cnt = 0;
            const promiseTest = new Promise((resolve, reject) => {
                Visualforce.remoting.Manager.invokeAction('{!$RemoteAction.VisualforcePageAsyncProcessController.getRecords}',
                async(result, event) => {
                    console.log(event.status);
                    if (event.status) {
                        document.getElementById("Size").innerText = "件数:" + result.length;
                        for (const element of result) {
                            cnt = cnt + 1;
                            document.getElementById("State").innerText = "ステータス:処理中";
                            if (!executedRecords.includes(element)) {
                                executedRecords += element + ",";
                            }
                        }
                        resolve(executedRecords);
                    }
                    else {
                        reject(event);
                    }
                });
            });
            promiseTest.then((value) => {
                document.getElementById("State").innerText = "ステータス:正常終了";
                Visualforce.remoting.Manager.invokeAction('{!$RemoteAction.VisualforcePageAsyncProcessController.setRecords}',
                value,
                async(result, event) => {
                    if (event.status) {
                        console.log("result:" + result);
                    }
                });
            }, reason => {
                document.getElementById("State").innerText = "ステータス:異常終了";
                console.error(reason);
            });
        </script>
    </body>
    </html>
</apex:page>

取引先を取得し、取得できたIdをまとめてサーバーへ返す画面です。apex:formを使わず、素のJavaScriptだけでリクエストを送っています。

Visualforce.remoting.timeout = 120000は120秒です。JavaScript Remotingのレスポンスタイムアウトは既定30秒で、延長できる上限が120秒までと公式ドキュメントに明記されています。このコードはちょうどその上限を指定しています。

Apexクラス

public with sharing class VisualforcePageAsyncProcessController {
    public VisualforcePageAsyncProcessController() {
    }

    @RemoteAction
    public static List<String> getRecords() {
        List<String> accList = new List<String>();
        for (Account acc : [SELECT Id, Name FROM Account LIMIT 10]) {
            accList.add(acc.Id);
        }
        return accList;
    }

    @RemoteAction
    public static void setRecords(String ids) {
        List<String> idParts = ids.split(',');
        List<Account> updateAccList = new List<Account>();
        for (Account accObj : [SELECT Id, VFProcessDatetime__c FROM Account WHERE Id IN :idParts]) {
            Account updateAccount = new Account();
            updateAccount.Id = accObj.Id;
            updateAccount.VFProcessDatetime__c = Datetime.now();
            updateAccList.add(updateAccount);
        }
        Savepoint sp = Database.setSavepoint();
        try {
            if (updateAccList.size() > 0) {
                update updateAccList;
            }
        } catch (DmlException e) {
            System.debug('The following exception has occurred: ' + e.getMessage());
            Database.rollback(sp);
        }
    }
}

@RemoteActionを付けたメソッドが、JavaScriptから直接呼べるエンドポイントになります。

JavaScript Remotingが非同期にしているもの

JavaScript Remotingが非同期にしているのは、ブラウザとサーバーの間の通信だけです。getRecordsやsetRecords自身は、リクエストを受け取った側で最初から最後まで同期的に実行されます。公式の制約事項にも「JavaScript remoting calls aren't subject to API limits, but Visualforce pages that use JavaScript remoting are subject to all standard Visualforce limits」とあり、通常のVisualforceページと同じガバナ制限の中で動く、と明記されています。

@future・Queueable Apex・Batch Apexが指す「非同期Apex」は、これとは別の話です。呼び出し元のトランザクションとは別に、ジョブとしてキューへ積まれて実行され、AsyncApexJobというレコードとして状態を追いかけられます。

JavaScript Remoting(このコード)非同期Apex(@future・Queueable・Batch)
実行されるタイミングリクエストを受け取った通常のトランザクション内別のトランザクションのジョブとして
ガバナ制限通常のVisualforceページと同じヒープサイズなど一部が緩和される
途中経過の追跡レスポンスの中身をJavaScriptで加工するしかないAsyncApexJobをクエリして進捗を取得できる
ページを閉じたらpromiseTestのチェーンが止まり、後続のsetRecordsは呼ばれないジョブはキューに残ったまま実行され続ける
このコードのpromiseTest.then()は、getRecordsのレスポンスを受け取れて初めてsetRecordsを呼びます。ページを閉じるなどしてレスポンスを受け取れなければ、取引先の更新(setRecords)はそのまま実行されません。
取引先の一覧。右端のVF処理日付列が赤枠で囲まれ、20時11分が並ぶ
実行前の一覧です。赤枠のVF処理日付には、前回の実行時刻がそのまま残っています。
取引先の一覧。赤枠のVF処理日付が20時20分に書き換わっている
実行後の一覧です。赤枠のVF処理日付が新しい時刻に書き換わっていれば、setRecordsまで通っています。

進捗を画面へ返したいときの構成

件数が多く、処理そのものを本当にバックグラウンドのジョブにしたい場合は、@RemoteActionの中でQueueable ApexをSystem.enqueueJobで登録し、返ってきたジョブIdを画面へ渡します。

@RemoteAction
public static Id startProcess(List<Id> targetIds) {
    return System.enqueueJob(new AccountUpdateQueueable(targetIds));
}

Queueableのジョブを登録し、ジョブIdを画面へ返す例です。

進捗は、別の@RemoteActionからAsyncApexJobを照会して返します。公式のBatch Apexの解説でも、進捗確認に同じ項目を使ったクエリが例として示されています。

@RemoteAction
public static AsyncApexJob checkProgress(Id jobId) {
    return [
        SELECT Id, Status, NumberOfErrors, JobItemsProcessed, TotalJobItems
        FROM AsyncApexJob
        WHERE Id = :jobId
    ];
}

ジョブの状態・処理済み件数・総件数・エラー件数をまとめて取得します。

画面側はapex:actionPollerやsetIntervalでcheckProgressを数秒おきに呼び、Statusが'Completed'になったら表示を切り替えます。この記事の最初のコードのように、JavaScriptだけで結果を組み立ててthenでつなぐ形では、この途中経過は取れません。

apex:actionFunctionとapex:actionStatusという選択肢

このコードは素のJavaScriptでVisualforce.remoting.Manager.invokeActionを直接呼んでいますが、apex:formを使う通常のページなら、apex:actionFunctionでコントローラーのメソッドをJavaScriptから呼び、apex:actionStatusで処理中・完了の表示を自動的に切り替えられます。

<apex:actionFunction name="startProcess" action="{!startProcess}" status="procStatus" reRender="result" />
<apex:actionStatus id="procStatus" startText="処理中です" stopText="完了しました" />
<div id="result">
    <apex:outputText value="{!resultText}" />
</div>

フォームを使う一般的なVisualforceページで、進捗表示を宣言的に書く例です。

apex:actionFunctionのstatus属性にapex:actionStatusのidを指定するだけで、リクエストの開始・終了に合わせてstartText・stopTextが自動的に表示されます。このコードのように、innerTextを自分で書き換える処理をJavaScript側に持つ必要がありません。

ここで間違えやすい

間違い何が起きるか
JavaScript Remotingを「非同期Apex」と同じ意味で使うAsyncApexJobには現れず、ガバナ制限も通常のページと同じままです
タイムアウトを120秒より長く設定する公式の上限を超えるため、意図通りに延長されません
promiseTestのチェーンが止まる可能性を考えないページを閉じると後続の更新処理が呼ばれないまま終わります
進捗を画面へ返したいのにQueueableへ渡さないAsyncApexJobを照会する対象がなく、進捗を追跡できません

確認した環境

  • 2026年9月/Salesforce Summer '26(APIバージョン67.0)時点の公式ドキュメントで、JavaScript Remotingの制約・非同期Apexの種類・AsyncApexJobの照会方法を確認しています
  • コードはAPIバージョン67.0で書いています

まとめ

  • JavaScript Remotingが非同期にするのはブラウザとサーバーの通信だけで、Apexの処理自体は通常のページと同じガバナ制限の中で同期的に動きます
  • @future・Queueable・Batch Apexが指す非同期Apexは別のトランザクションのジョブで、AsyncApexJobから状態を照会できます
  • 進捗を画面へ返したいときは、Queueableのジョブを登録してジョブIdを受け取り、別のリクエストでAsyncApexJobを照会します
  • apex:formを使う通常のページなら、apex:actionFunctionとapex:actionStatusの組み合わせで進捗表示を宣言的に書けます
  • JavaScript Remotingのタイムアウトは既定30秒、延長できる上限は120秒です

参考:当時の画面

記事を最初に書いた当時の画面です。いまの手順と違うところは、各画像の説明に書いています。

ブラウザのコンソール。cnt:1からcnt:10までのログが赤枠で並んでいる
処理中のコンソールです。10件を1件ずつ数えている様子が見えます。

Salesforceの導入・運用についてご相談ください

導入前の検討から、お使いの環境の改修・運用、AIとの連携まで承ります。状況を伺ったうえで、進め方をご提案します。