MicrosoftEdge / MicrosoftEdge/WebView2Feedback

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

Open
#5,557 2 comments 0 reactions 1 assignee View on GitHub

@prija-microsoft is already working on this.

Since Apr 22, 2026.

bug
Dominant language
PowerShell
Stars
526
Forks
67
PR merge metrics
No merged PRs in 30d

Description

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

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.