Skip to content

Commit

Permalink
Replaced network.onRequestFinished() caching with network.getHAR()
Browse files Browse the repository at this point in the history
This avoids the need for us to redundantly (pre) cache JavaScript content. In the event that the HAR log doesn't contain a match, we'll fall back to fetching from the Network (and hoping for a cache hit from that layer).
  • Loading branch information
Brian Vaughn committed Sep 10, 2021
1 parent 0fd195f commit 90bd228
Showing 1 changed file with 116 additions and 65 deletions.
181 changes: 116 additions & 65 deletions packages/react-devtools-extensions/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,13 @@ import {
localStorageSetItem,
} from 'react-devtools-shared/src/storage';
import DevTools from 'react-devtools-shared/src/devtools/views/DevTools';
import {__DEBUG__} from 'react-devtools-shared/src/constants';

const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY =
'React::DevTools::supportsProfiling';

const isChrome = getBrowserName() === 'Chrome';

const cachedNetworkEvents = new Map();

// Cache JavaScript resources as the page loads them.
// This helps avoid unnecessary duplicate requests when hook names are parsed.
// Responses with a Vary: 'Origin' might not match future requests.
// This lets us avoid a possible (expensive) cache miss.
// For more info see: github.com/facebook/react/pull/22198
chrome.devtools.network.onRequestFinished.addListener(
function onRequestFinished(event) {
if (event.request.method === 'GET') {
switch (event.response.content.mimeType) {
case 'application/javascript':
case 'application/x-javascript':
case 'text/javascript':
cachedNetworkEvents.set(event.request.url, event);
break;
}
}
},
);

let panelCreated = false;

// The renderer interface can't read saved component filters directly,
Expand Down Expand Up @@ -233,56 +213,113 @@ function createPanelIfReactLoaded() {
}
};

let debugIDCounter = 0;

// For some reason in Firefox, chrome.runtime.sendMessage() from a content script
// never reaches the chrome.runtime.onMessage event listener.
let fetchFileWithCaching = null;
if (isChrome) {
// Fetching files from the extension won't make use of the network cache
// for resources that have already been loaded by the page.
// This helper function allows the extension to request files to be fetched
// by the content script (running in the page) to increase the likelihood of a cache hit.
fetchFileWithCaching = url => {
const event = cachedNetworkEvents.get(url);
if (event != null) {
// If this resource has already been cached locally,
// skip the network queue (which might not be a cache hit anyway)
// and just use the cached response.
return new Promise(resolve => {
event.getContent(content => resolve(content));
});
const fetchFromNetworkCache = (url, resolve, reject) => {
// Debug ID allows us to avoid re-logging (potentially long) URL strings below,
// while also still associating (potentially) interleaved logs with the original request.
let debugID = null;

if (__DEBUG__) {
debugID = debugIDCounter++;
console.log(`[main] fetchFromNetworkCache(${debugID})`, url);
}

// If DevTools was opened after the page started loading,
// we may have missed some requests.
// So fall back to a fetch() and hope we get a cached response.

return new Promise((resolve, reject) => {
function onPortMessage({payload, source}) {
if (source === 'react-devtools-content-script') {
switch (payload?.type) {
case 'fetch-file-with-cache-complete':
chrome.runtime.onMessage.removeListener(onPortMessage);
resolve(payload.value);
break;
case 'fetch-file-with-cache-error':
chrome.runtime.onMessage.removeListener(onPortMessage);
reject(payload.value);
break;
chrome.devtools.network.getHAR(harLog => {
for (let i = 0; i < harLog.entries.length; i++) {
const entry = harLog.entries[i];
if (url === entry.request.url) {
if (__DEBUG__) {
console.log(
`[main] fetchFromNetworkCache(${debugID}) Found matching URL in HAR`,
url,
);
}

entry.getContent(content => {
if (content) {
if (__DEBUG__) {
console.log(
`[main] fetchFromNetworkCache(${debugID}) Content retrieved`,
);
}

resolve(content);
} else {
if (__DEBUG__) {
console.log(
`[main] fetchFromNetworkCache(${debugID}) Invalid content returned by getContent()`,
content,
);
}

// Edge case where getContent() returned null; fall back to fetch.
fetchFromPage(url, resolve);
}
});

return;
}
}

chrome.runtime.onMessage.addListener(onPortMessage);
if (__DEBUG__) {
console.log(
`[main] fetchFromNetworkCache(${debugID}) No cached request found in getHAR()`,
);
}

chrome.devtools.inspectedWindow.eval(`
window.postMessage({
source: 'react-devtools-extension',
payload: {
type: 'fetch-file-with-cache',
url: "${url}",
},
});
`);
// No matching URL found; fall back to fetch.
fetchFromPage(url, resolve);
});
};

const fetchFromPage = (url, resolve, reject) => {
if (__DEBUG__) {
console.log('[main] fetchFromPage()', url);
}

function onPortMessage({payload, source}) {
if (source === 'react-devtools-content-script') {
switch (payload?.type) {
case 'fetch-file-with-cache-complete':
chrome.runtime.onMessage.removeListener(onPortMessage);
resolve(payload.value);
break;
case 'fetch-file-with-cache-error':
chrome.runtime.onMessage.removeListener(onPortMessage);
reject(payload.value);
break;
}
}
}

chrome.runtime.onMessage.addListener(onPortMessage);

chrome.devtools.inspectedWindow.eval(`
window.postMessage({
source: 'react-devtools-extension',
payload: {
type: 'fetch-file-with-cache',
url: "${url}",
},
});
`);
};

// Fetching files from the extension won't make use of the network cache
// for resources that have already been loaded by the page.
// This helper function allows the extension to request files to be fetched
// by the content script (running in the page) to increase the likelihood of a cache hit.
fetchFileWithCaching = url => {
return new Promise((resolve, reject) => {
// Try fetching from the Network cache first.
// If DevTools was opened after the page started loading, we may have missed some requests.
// So fall back to a fetch() from the page and hope we get a cached response that way.
fetchFromNetworkCache(url, resolve, reject);
});
};
}
Expand Down Expand Up @@ -441,9 +478,6 @@ function createPanelIfReactLoaded() {

// Re-initialize DevTools panel when a new page is loaded.
chrome.devtools.network.onNavigated.addListener(function onNavigated() {
// Clear cached requests when a new page is opened.
cachedNetworkEvents.clear();

// Re-initialize saved filters on navigation,
// since global values stored on window get reset in this case.
syncSavedPreferences();
Expand All @@ -460,8 +494,7 @@ function createPanelIfReactLoaded() {

// Load (or reload) the DevTools extension when the user navigates to a new page.
function checkPageForReact() {
// Clear cached requests when a new page is opened.
cachedNetworkEvents.clear();
cachedNetworkRequests.clear();

syncSavedPreferences();
createPanelIfReactLoaded();
Expand All @@ -472,7 +505,25 @@ chrome.devtools.network.onNavigated.addListener(checkPageForReact);
// Check to see if React has loaded once per second in case React is added
// after page load
const loadCheckInterval = setInterval(function() {
cachedNetworkRequests.clear();

createPanelIfReactLoaded();
}, 1000);

createPanelIfReactLoaded();

const cachedNetworkRequests = new Map();

chrome.devtools.network.onRequestFinished.addListener(
function onRequestFinished(request) {
if (request.request.method === 'GET') {
switch (request.response.content.mimeType) {
case 'application/javascript':
case 'application/x-javascript':
case 'text/javascript':
cachedNetworkRequests.set(request.request.url, request);
break;
}
}
},
);

0 comments on commit 90bd228

Please sign in to comment.