-
Notifications
You must be signed in to change notification settings - Fork 63
[Problem/Bug]: CreateCoreWebView2ControllerWithOptions is ~12x slower when a non-default profile name is specified #5557
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";
case 2, kProfileName = nullptr;
