Skip to content

[Problem/Bug]: CreateCoreWebView2ControllerWithOptions is ~12x slower when a non-default profile name is specified #5557

@VincentZheng520

Description

@VincentZheng520

Summary

When calling ICoreWebView2Environment10::CreateCoreWebView2ControllerWithOptions, specifying a non-default profile name causes controller creation to be dramatically slower compared to using the default profile. Both test cases use the same API; the only difference is the value passed to put_ProfileName.

Environment

WebView2 Runtime Version 146.0.3856.97
OS Name Microsoft Windows 11 Pro
OS Version 10.0.26200 N/A Build 26200
User Data Dir Fresh directory (D:\temp\wv2-test)

Steps to Reproduce

Both cases use CreateCoreWebView2ControllerWithOptions with IsInPrivateModeEnabled = FALSE. The only variable is ProfileName.

Case 1 — Default profile (ProfileName = nullptr):

ComPtr<ICoreWebView2ControllerOptions> opts;
env10->CreateCoreWebView2ControllerOptions(&opts);
opts->put_ProfileName(nullptr);  // default profile
opts->put_IsInPrivateModeEnabled(FALSE);
env10->CreateCoreWebView2ControllerWithOptions(hwnd, opts.Get(), handler);

Case 2 — Named profile (ProfileName = "Vincent"):

ComPtr<ICoreWebView2ControllerOptions> opts;
env10->CreateCoreWebView2ControllerOptions(&opts);
opts->put_ProfileName(L"Vincent");  // named profile
opts->put_IsInPrivateModeEnabled(FALSE);
env10->CreateCoreWebView2ControllerWithOptions(hwnd, opts.Get(), handler);

Measured Results

Test ProfileName env_ms controller_ms total_ms
Case 1 (default) 10.26 ms 420.42 ms 488.48 ms
Case 2 "Vincent" 9.94 ms 5398.61 ms 5466.97 ms
  • Environment creation time is negligible and roughly equal in both cases (~10 ms).
  • Controller creation time is ~12.8× slower when a named profile is specified.
  • Both tests used a fresh user data directory (cold start).

Expected Behavior

Controller creation time should not differ significantly based on whether a default or named profile is used. Any additional first-time initialization cost for a new named profile should be in the order of milliseconds, not seconds.

Actual Behavior

Specifying a non-default profile name causes CreateCoreWebView2ControllerWithOptions to block for approximately 5 seconds on cold start, making it impractical for any latency-sensitive use case.

Additional Notes

  • The overhead is entirely in the controller creation step; environment creation is unaffected.
  • We have not yet tested warm-start behavior (subsequent creations reusing the same named profile).
  • Reproduces consistently across multiple runs.

Test code

#define NOMINMAX
#include <windows.h>
#include <wrl.h>
#include <shlwapi.h>
#include <WebView2.h>

#include <chrono>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>

#pragma comment(lib, "Shlwapi.lib")

using Microsoft::WRL::Callback;
using Microsoft::WRL::ComPtr;

// ===== configuration =====
static constexpr const wchar_t* kUserDataDir = L"D:\\temp\\wv2-test";
static constexpr const wchar_t* kRuntimePath = nullptr;  // nullptr = system runtime
static constexpr const wchar_t* kProfileName = L"Vincent";  // nullptr = default profile
static constexpr bool           kIsInPrivate = false;
static constexpr bool           kNoNavigate  = false;
// =========================

namespace
{
using Clock = std::chrono::steady_clock;

struct State
{
    Clock::time_point appStart;
    Clock::time_point envStart;
    Clock::time_point ctrlStart;

    HWND hwnd = nullptr;
    HMODULE loaderModule = nullptr;
    ComPtr<ICoreWebView2Environment> environment;
    ComPtr<ICoreWebView2Controller> controller;
    ComPtr<ICoreWebView2> webview;
    HRESULT lastError = S_OK;
};

using CreateCoreWebView2EnvironmentWithOptionsFn =
    HRESULT(STDAPICALLTYPE*)(PCWSTR, PCWSTR, ICoreWebView2EnvironmentOptions*,
                             ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler*);

double Ms(const Clock::time_point& a, const Clock::time_point& b)
{
    return std::chrono::duration<double, std::milli>(b - a).count();
}

std::wstring Hr(HRESULT hr)
{
    std::wstringstream ss;
    ss << L"0x" << std::hex << std::uppercase << static_cast<unsigned long>(hr);
    return ss.str();
}

std::wstring GetExeDir()
{
    wchar_t path[MAX_PATH] = {};
    GetModuleFileNameW(nullptr, path, static_cast<DWORD>(std::size(path)));
    PathRemoveFileSpecW(path);
    return path;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
    if (msg == WM_DESTROY) { PostQuitMessage(0); return 0; }
    return DefWindowProcW(hwnd, msg, wp, lp);
}

HWND CreateHiddenWindow(HINSTANCE inst)
{
    WNDCLASSW wc = {};
    wc.lpfnWndProc   = WndProc;
    wc.hInstance     = inst;
    wc.lpszClassName = L"WV2TimingTester";
    RegisterClassW(&wc);
    return CreateWindowExW(0, L"WV2TimingTester", L"WV2 Timing Tester",
        WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
        nullptr, nullptr, inst, nullptr);
}

void CreateController(State& state)
{
    state.ctrlStart = Clock::now();
    std::wcout << L"[CTRL_START]" << std::endl;

    auto done = Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
        [&state](HRESULT result, ICoreWebView2Controller* ctrl) -> HRESULT
        {
            const auto now = Clock::now();
            std::wcout << std::fixed << std::setprecision(2)
                       << L"[CTRL_DONE ] hr=" << Hr(result)
                       << L" controller_ms=" << Ms(state.ctrlStart, now)
                       << L" total_ms="      << Ms(state.appStart,  now)
                       << std::endl;

            if (FAILED(result) || !ctrl)
            {
                state.lastError = FAILED(result) ? result : E_FAIL;
                PostQuitMessage(1);
                return S_OK;
            }

            state.controller = ctrl;
            state.controller->get_CoreWebView2(&state.webview);

            RECT bounds = {};
            GetClientRect(state.hwnd, &bounds);
            state.controller->put_Bounds(bounds);

            if (!kNoNavigate)
                state.webview->Navigate(L"https://www.google.com");

            PostQuitMessage(0);
            return S_OK;
        });

    HRESULT hr = S_OK;
    const bool useProfile = (kProfileName != nullptr) || kIsInPrivate;

    if (useProfile)
    {
        ComPtr<ICoreWebView2Environment10> env10;
        if (FAILED(state.environment.As(&env10)) || !env10)
        {
            std::wcerr << L"[FAIL] ICoreWebView2Environment10 not available\n";
            PostQuitMessage(1);
            return;
        }
        ComPtr<ICoreWebView2ControllerOptions> opts;
        env10->CreateCoreWebView2ControllerOptions(&opts);
        if (kProfileName) opts->put_ProfileName(kProfileName);
        opts->put_IsInPrivateModeEnabled(kIsInPrivate ? TRUE : FALSE);
        hr = env10->CreateCoreWebView2ControllerWithOptions(state.hwnd, opts.Get(), done.Get());
    }
    else
    {
        hr = state.environment->CreateCoreWebView2Controller(state.hwnd, done.Get());
    }

    if (FAILED(hr))
    {
        std::wcerr << L"[FAIL] CreateCoreWebView2Controller hr=" << Hr(hr) << L"\n";
        state.lastError = hr;
        PostQuitMessage(1);
    }
}

HRESULT StartEnvironment(State& state)
{
    const std::wstring exeDir = GetExeDir();
    for (auto& candidate : { exeDir + L"\\WebView2Loader.dll", std::wstring(L"WebView2Loader.dll") })
    {
        state.loaderModule = LoadLibraryW(candidate.c_str());
        if (state.loaderModule)
        {
            std::wcout << L"[LOADER    ] " << candidate << std::endl;
            break;
        }
    }
    if (!state.loaderModule)
        return HRESULT_FROM_WIN32(GetLastError());

    auto createEnv = reinterpret_cast<CreateCoreWebView2EnvironmentWithOptionsFn>(
        GetProcAddress(state.loaderModule, "CreateCoreWebView2EnvironmentWithOptions"));
    if (!createEnv)
        return HRESULT_FROM_WIN32(GetLastError());

    state.envStart = Clock::now();
    std::wcout << L"[ENV_START ]" << std::endl;

    return createEnv(
        kRuntimePath, kUserDataDir, nullptr,
        Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
            [&state](HRESULT result, ICoreWebView2Environment* env) -> HRESULT
            {
                std::wcout << std::fixed << std::setprecision(2)
                           << L"[ENV_DONE  ] hr=" << Hr(result)
                           << L" env_ms=" << Ms(state.envStart, Clock::now())
                           << std::endl;

                if (FAILED(result) || !env)
                {
                    state.lastError = FAILED(result) ? result : E_FAIL;
                    PostQuitMessage(1);
                    return S_OK;
                }

                state.environment = env;

                LPWSTR ver = nullptr;
                if (SUCCEEDED(env->get_BrowserVersionString(&ver)) && ver)
                {
                    std::wcout << L"[VERSION   ] " << ver << std::endl;
                    CoTaskMemFree(ver);
                }

                CreateController(state);
                return S_OK;
            }).Get());
}

} // namespace

int main()
{
    State state;
    state.appStart = Clock::now();

    std::wcout
        << L"WebView2 Controller Timing Tester\n"
        << L"  userDataDir : " << (kUserDataDir ? kUserDataDir : L"<default>") << L"\n"
        << L"  runtimePath : " << (kRuntimePath ? kRuntimePath : L"<system>") << L"\n"
        << L"  profile     : " << (kProfileName ? kProfileName : L"<default>") << L"\n"
        << L"  inPrivate   : " << (kIsInPrivate ? L"true" : L"false") << L"\n"
        << L"  noNavigate  : " << (kNoNavigate  ? L"true" : L"false") << L"\n"
        << std::endl;

    if (kUserDataDir)
    {
        std::error_code ec;
        std::filesystem::create_directories(kUserDataDir, ec);
    }

    if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED)))
        return 1;

    state.hwnd = CreateHiddenWindow(GetModuleHandleW(nullptr));
    if (!state.hwnd) { CoUninitialize(); return 1; }
    ShowWindow(state.hwnd, SW_SHOW);
    UpdateWindow(state.hwnd);

    if (FAILED(StartEnvironment(state)))
    {
        DestroyWindow(state.hwnd);
        if (state.loaderModule) FreeLibrary(state.loaderModule);
        CoUninitialize();
        return 1;
    }

    MSG msg = {};
    while (GetMessageW(&msg, nullptr, 0, 0) > 0)
    {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }

    if (state.controller) state.controller->Close();
    if (state.hwnd)       DestroyWindow(state.hwnd);
    if (state.loaderModule) FreeLibrary(state.loaderModule);
    CoUninitialize();

    return FAILED(state.lastError) ? 1 : static_cast<int>(msg.wParam);
}

Console output

case 1, kProfileName = L"vincent";

Image

case 2, kProfileName = nullptr;

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions