mirror of
https://github.com/facebook/react.git
synced 2026-02-25 05:03:03 +00:00
While testing the recently-launched named hooks feature, I noticed that one of the two big performance bottlenecks is fetching the source file. This was unexpected since the source file has already been loaded by the page. (After all, DevTools is inspecting a component defined in that same file.) To address this, I made the following changes: - [x] Keep CPU bound work (parsing source map and AST) in a worker so it doesn't block the main thread but move I/O bound code (fetching files) to the main thread. - [x] Inject a function into the page (as part of the content script) to fetch cached files for the extension. Communicate with this function using `eval()` (to send it messages) and `chrome.runtime.sendMessage()` to return its responses to the extension). With the above changes in place, the extension gets cached responses from a lot of sites- but not Facebook. This seems to be due to the following: * Facebook's response headers include [`vary: 'Origin'`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary). * The `fetch` made from the content script does not include an `Origin` request header. To reduce the impact of cases where we can't re-use the Network cache, this PR also makes additional changes: - [x] Use `devtools.network.onRequestFinished` to (pre)cache resources as the page loads them. This allows us to avoid requesting a resource that's already been loaded in most cases. - [x] In case DevTools was opened _after_ some requests were made, we also now pre-fetch (and cache in memory) source files when a component is selected (if it has hooks). If the component's hooks are later evaluated, the source map will be faster to access. (Note that in many cases, this prefetch is very fast since it is returned from the disk cache.) With the above changes, we've reduced the time spent in `loadSourceFiles` to nearly nothing.
190 lines
4.7 KiB
JavaScript
190 lines
4.7 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import {__DEBUG__} from 'react-devtools-shared/src/constants';
|
|
|
|
import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
|
|
import type {Thenable, Wakeable} from 'shared/ReactTypes';
|
|
import type {Element} from './devtools/views/Components/types';
|
|
import type {
|
|
HookNames,
|
|
HookSourceLocationKey,
|
|
} from 'react-devtools-shared/src/types';
|
|
import type {HookSource} from 'react-debug-tools/src/ReactDebugHooks';
|
|
import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/DevTools';
|
|
|
|
const TIMEOUT = 30000;
|
|
|
|
const Pending = 0;
|
|
const Resolved = 1;
|
|
const Rejected = 2;
|
|
|
|
type PendingRecord = {|
|
|
status: 0,
|
|
value: Wakeable,
|
|
|};
|
|
|
|
type ResolvedRecord<T> = {|
|
|
status: 1,
|
|
value: T,
|
|
|};
|
|
|
|
type RejectedRecord = {|
|
|
status: 2,
|
|
value: null,
|
|
|};
|
|
|
|
type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
|
|
|
|
function readRecord<T>(record: Record<T>): ResolvedRecord<T> | RejectedRecord {
|
|
if (record.status === Resolved) {
|
|
// This is just a type refinement.
|
|
return record;
|
|
} else if (record.status === Rejected) {
|
|
// This is just a type refinement.
|
|
return record;
|
|
} else {
|
|
throw record.value;
|
|
}
|
|
}
|
|
|
|
type LoadHookNamesFunction = (
|
|
hookLog: HooksTree,
|
|
fetchFileWithCaching: FetchFileWithCaching | null,
|
|
) => Thenable<HookNames>;
|
|
|
|
// This is intentionally a module-level Map, rather than a React-managed one.
|
|
// Otherwise, refreshing the inspected element cache would also clear this cache.
|
|
// TODO Rethink this if the React API constraints change.
|
|
// See https://github.com/reactwg/react-18/discussions/25#discussioncomment-980435
|
|
let map: WeakMap<Element, Record<HookNames>> = new WeakMap();
|
|
|
|
export function hasAlreadyLoadedHookNames(element: Element): boolean {
|
|
const record = map.get(element);
|
|
return record != null && record.status === Resolved;
|
|
}
|
|
|
|
export function loadHookNames(
|
|
element: Element,
|
|
hooksTree: HooksTree,
|
|
loadHookNamesFunction: LoadHookNamesFunction,
|
|
fetchFileWithCaching: FetchFileWithCaching | null,
|
|
): HookNames | null {
|
|
let record = map.get(element);
|
|
|
|
if (__DEBUG__) {
|
|
console.groupCollapsed('loadHookNames() record:');
|
|
console.log(record);
|
|
console.groupEnd();
|
|
}
|
|
|
|
if (!record) {
|
|
const callbacks = new Set();
|
|
const wakeable: Wakeable = {
|
|
then(callback) {
|
|
callbacks.add(callback);
|
|
},
|
|
};
|
|
|
|
const wake = () => {
|
|
if (timeoutID) {
|
|
clearTimeout(timeoutID);
|
|
timeoutID = null;
|
|
}
|
|
|
|
// This assumes they won't throw.
|
|
callbacks.forEach(callback => callback());
|
|
callbacks.clear();
|
|
};
|
|
|
|
const newRecord: Record<HookNames> = (record = {
|
|
status: Pending,
|
|
value: wakeable,
|
|
});
|
|
|
|
let didTimeout = false;
|
|
|
|
loadHookNamesFunction(hooksTree, fetchFileWithCaching).then(
|
|
function onSuccess(hookNames) {
|
|
if (didTimeout) {
|
|
return;
|
|
}
|
|
|
|
if (__DEBUG__) {
|
|
console.log('[hookNamesCache] onSuccess() hookNames:', hookNames);
|
|
}
|
|
|
|
if (hookNames) {
|
|
const resolvedRecord = ((newRecord: any): ResolvedRecord<HookNames>);
|
|
resolvedRecord.status = Resolved;
|
|
resolvedRecord.value = hookNames;
|
|
} else {
|
|
const notFoundRecord = ((newRecord: any): RejectedRecord);
|
|
notFoundRecord.status = Rejected;
|
|
notFoundRecord.value = null;
|
|
}
|
|
|
|
wake();
|
|
},
|
|
function onError(error) {
|
|
if (didTimeout) {
|
|
return;
|
|
}
|
|
|
|
if (__DEBUG__) {
|
|
console.log('[hookNamesCache] onError() error:', error);
|
|
}
|
|
|
|
const thrownRecord = ((newRecord: any): RejectedRecord);
|
|
thrownRecord.status = Rejected;
|
|
thrownRecord.value = null;
|
|
|
|
wake();
|
|
},
|
|
);
|
|
|
|
// Eventually timeout and stop trying to load names.
|
|
let timeoutID = setTimeout(function onTimeout() {
|
|
if (__DEBUG__) {
|
|
console.log('[hookNamesCache] onTimeout()');
|
|
}
|
|
|
|
timeoutID = null;
|
|
|
|
didTimeout = true;
|
|
|
|
const timedoutRecord = ((newRecord: any): RejectedRecord);
|
|
timedoutRecord.status = Rejected;
|
|
timedoutRecord.value = null;
|
|
|
|
wake();
|
|
}, TIMEOUT);
|
|
|
|
map.set(element, record);
|
|
}
|
|
|
|
const response = readRecord(record).value;
|
|
return response;
|
|
}
|
|
|
|
export function getHookSourceLocationKey({
|
|
fileName,
|
|
lineNumber,
|
|
columnNumber,
|
|
}: HookSource): HookSourceLocationKey {
|
|
if (fileName == null || lineNumber == null || columnNumber == null) {
|
|
throw Error('Hook source code location not found.');
|
|
}
|
|
return `${fileName}:${lineNumber}:${columnNumber}`;
|
|
}
|
|
|
|
export function clearHookNamesCache(): void {
|
|
map = new WeakMap();
|
|
}
|