-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: faster remote browser loading #860
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1f99295
58aa5f5
d7da903
cd53994
fcb3a12
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -201,6 +201,11 @@ export class RemoteBrowser { | |
| private networkRequestTimeout: NodeJS.Timeout | null = null; | ||
| private pendingNetworkRequests: string[] = []; | ||
| private readonly NETWORK_QUIET_PERIOD = 8000; | ||
| private readonly INITIAL_LOAD_QUIET_PERIOD = 3000; | ||
| private networkWaitStartTime: number = 0; | ||
| private progressInterval: NodeJS.Timeout | null = null; | ||
| private hasShownInitialLoader: boolean = false; | ||
| private isInitialLoadInProgress: boolean = false; | ||
|
|
||
| /** | ||
| * Initializes a new instances of the {@link Generator} and {@link WorkflowInterpreter} classes and | ||
|
|
@@ -432,42 +437,74 @@ export class RemoteBrowser { | |
| if (!this.currentPage) return; | ||
|
|
||
| this.currentPage.on("domcontentloaded", async () => { | ||
| logger.info("DOM content loaded - triggering snapshot"); | ||
| await this.makeAndEmitDOMSnapshot(); | ||
| if (!this.isInitialLoadInProgress) { | ||
| logger.info("DOM content loaded - triggering snapshot"); | ||
| await this.makeAndEmitDOMSnapshot(); | ||
| } | ||
| }); | ||
|
|
||
| this.currentPage.on("response", async (response) => { | ||
| const url = response.url(); | ||
| if ( | ||
| response.request().resourceType() === "document" || | ||
| url.includes("api/") || | ||
| url.includes("ajax") | ||
| ) { | ||
| const isDocumentRequest = response.request().resourceType() === "document"; | ||
|
|
||
| if (!this.hasShownInitialLoader && isDocumentRequest && !url.includes("about:blank")) { | ||
| this.hasShownInitialLoader = true; | ||
| this.isInitialLoadInProgress = true; | ||
| this.pendingNetworkRequests.push(url); | ||
|
|
||
| if (this.networkRequestTimeout) { | ||
| clearTimeout(this.networkRequestTimeout); | ||
| this.networkRequestTimeout = null; | ||
| } | ||
|
|
||
| if (this.progressInterval) { | ||
| clearInterval(this.progressInterval); | ||
| this.progressInterval = null; | ||
| } | ||
|
|
||
| this.networkWaitStartTime = Date.now(); | ||
| this.progressInterval = setInterval(() => { | ||
| const elapsed = Date.now() - this.networkWaitStartTime; | ||
| const navigationProgress = Math.min((elapsed / this.INITIAL_LOAD_QUIET_PERIOD) * 40, 35); | ||
| const totalProgress = 60 + navigationProgress; | ||
| this.emitLoadingProgress(totalProgress, this.pendingNetworkRequests.length); | ||
| }, 500); | ||
|
Comment on lines
+466
to
+471
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Memory leak: progressInterval not cleaned up in shutdown/cleanup methods. The Add cleanup in the relevant methods: // In switchOff() method around line 1738
if (this.progressInterval) {
clearInterval(this.progressInterval);
this.progressInterval = null;
}
// In stopDOM() method around line 1703
if (this.progressInterval) {
clearInterval(this.progressInterval);
this.progressInterval = null;
}🤖 Prompt for AI Agents |
||
|
|
||
| logger.debug( | ||
| `Network request received: ${url}. Total pending: ${this.pendingNetworkRequests.length}` | ||
| `Initial load network request received: ${url}. Using ${this.INITIAL_LOAD_QUIET_PERIOD}ms quiet period` | ||
| ); | ||
|
|
||
| this.networkRequestTimeout = setTimeout(async () => { | ||
| logger.info( | ||
| `Network quiet period reached. Processing ${this.pendingNetworkRequests.length} requests` | ||
| `Initial load network quiet period reached (${this.INITIAL_LOAD_QUIET_PERIOD}ms)` | ||
| ); | ||
|
|
||
| if (this.progressInterval) { | ||
| clearInterval(this.progressInterval); | ||
| this.progressInterval = null; | ||
| } | ||
|
|
||
| this.emitLoadingProgress(100, this.pendingNetworkRequests.length); | ||
|
|
||
| this.pendingNetworkRequests = []; | ||
| this.networkRequestTimeout = null; | ||
| this.isInitialLoadInProgress = false; | ||
|
|
||
| await this.makeAndEmitDOMSnapshot(); | ||
| }, this.NETWORK_QUIET_PERIOD); | ||
| }, this.INITIAL_LOAD_QUIET_PERIOD); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| private emitLoadingProgress(progress: number, pendingRequests: number): void { | ||
| this.socket.emit("domLoadingProgress", { | ||
| progress: Math.round(progress), | ||
| pendingRequests, | ||
| userId: this.userId, | ||
| timestamp: Date.now(), | ||
| }); | ||
| } | ||
|
|
||
| private async setupPageEventListeners(page: Page) { | ||
| page.on('framenavigated', async (frame) => { | ||
| if (frame === page.mainFrame()) { | ||
|
|
@@ -521,7 +558,13 @@ export class RemoteBrowser { | |
| const MAX_RETRIES = 3; | ||
| let retryCount = 0; | ||
| let success = false; | ||
|
|
||
|
|
||
| this.socket.emit("dom-snapshot-loading", { | ||
| userId: this.userId, | ||
| timestamp: Date.now(), | ||
| }); | ||
| this.emitLoadingProgress(0, 0); | ||
|
|
||
| while (!success && retryCount < MAX_RETRIES) { | ||
| try { | ||
| this.browser = <Browser>(await chromium.launch({ | ||
|
|
@@ -545,7 +588,9 @@ export class RemoteBrowser { | |
| if (!this.browser || this.browser.isConnected() === false) { | ||
| throw new Error('Browser failed to launch or is not connected'); | ||
| } | ||
|
|
||
|
|
||
| this.emitLoadingProgress(20, 0); | ||
|
|
||
| const proxyConfig = await getDecryptedProxyConfig(userId); | ||
| let proxyOptions: { server: string, username?: string, password?: string } = { server: '' }; | ||
|
|
||
|
|
@@ -623,6 +668,8 @@ export class RemoteBrowser { | |
|
|
||
| this.currentPage = await this.context.newPage(); | ||
|
|
||
| this.emitLoadingProgress(40, 0); | ||
|
|
||
| await this.setupPageEventListeners(this.currentPage); | ||
|
|
||
| const viewportSize = await this.currentPage.viewportSize(); | ||
|
|
@@ -645,7 +692,9 @@ export class RemoteBrowser { | |
| // Still need to set up the CDP session even if blocker fails | ||
| this.client = await this.currentPage.context().newCDPSession(this.currentPage); | ||
| } | ||
|
|
||
|
|
||
| this.emitLoadingProgress(60, 0); | ||
|
|
||
| success = true; | ||
| logger.log('debug', `Browser initialized successfully for user ${userId}`); | ||
| } catch (error: any) { | ||
|
|
@@ -1521,9 +1570,6 @@ export class RemoteBrowser { | |
| this.isDOMStreamingActive = true; | ||
| logger.info("DOM streaming started successfully"); | ||
|
|
||
| // Initial DOM snapshot | ||
| await this.makeAndEmitDOMSnapshot(); | ||
|
|
||
| this.setupScrollEventListener(); | ||
| this.setupPageChangeListeners(); | ||
| } catch (error) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,7 @@ import { | |
| export const BrowserContent = () => { | ||
| const { socket } = useSocketStore(); | ||
|
|
||
| const [tabs, setTabs] = useState<string[]>(["current"]); | ||
| const [tabs, setTabs] = useState<string[]>(["Loading..."]); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prevent phantom “Loading...” tab from breaking tab indices. By keeping the placeholder when Apply this diff to restore the previous behavior: - if (response && response.length > 0) {
- setTabs(response);
- }
+ if (Array.isArray(response)) {
+ setTabs(response);
+ }Also applies to: 128-130 🤖 Prompt for AI Agents |
||
| const [tabIndex, setTabIndex] = React.useState(0); | ||
| const [showOutputData, setShowOutputData] = useState(false); | ||
| const { browserWidth } = useBrowserDimensionsStore(); | ||
|
|
@@ -125,7 +125,7 @@ export const BrowserContent = () => { | |
| useEffect(() => { | ||
| getCurrentTabs() | ||
| .then((response) => { | ||
| if (response) { | ||
| if (response && response.length > 0) { | ||
| setTabs(response); | ||
| } | ||
| }) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fragile URL check could match unintended substrings.
Using
url.includes("about:blank")could incorrectly match legitimate URLs that contain "about:blank" as a substring (e.g.,https://example.com/page?redirect=about:blank).Consider using a more precise check:
📝 Committable suggestion