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

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