@@ -206,6 +206,7 @@ class URLContext {
206206 }
207207}
208208
209+ let setURLSearchParamsModified ;
209210let setURLSearchParamsContext ;
210211let getURLSearchParamsList ;
211212let setURLSearchParams ;
@@ -475,8 +476,9 @@ class URLSearchParams {
475476 name = StringPrototypeToWellFormed ( `${ name } ` ) ;
476477 value = StringPrototypeToWellFormed ( `${ value } ` ) ;
477478 ArrayPrototypePush ( this . #searchParams, name , value ) ;
479+
478480 if ( this . #context) {
479- this . #context. search = this . toString ( ) ;
481+ setURLSearchParamsModified ( this . #context) ;
480482 }
481483 }
482484
@@ -509,8 +511,9 @@ class URLSearchParams {
509511 }
510512 }
511513 }
514+
512515 if ( this . #context) {
513- this . #context. search = this . toString ( ) ;
516+ setURLSearchParamsModified ( this . #context) ;
514517 }
515518 }
516519
@@ -615,7 +618,7 @@ class URLSearchParams {
615618 }
616619
617620 if ( this . #context) {
618- this . #context. search = this . toString ( ) ;
621+ setURLSearchParamsModified ( this . #context) ;
619622 }
620623 }
621624
@@ -664,7 +667,7 @@ class URLSearchParams {
664667 }
665668
666669 if ( this . #context) {
667- this . #context. search = this . toString ( ) ;
670+ setURLSearchParamsModified ( this . #context) ;
668671 }
669672 }
670673
@@ -769,6 +772,20 @@ function isURL(self) {
769772class URL {
770773 #context = new URLContext ( ) ;
771774 #searchParams;
775+ #searchParamsModified;
776+
777+ static {
778+ setURLSearchParamsModified = ( obj ) => {
779+ // When URLSearchParams changes, we lazily update URL on the next read/write for performance.
780+ obj . #searchParamsModified = true ;
781+
782+ // If URL has an existing search, remove it without cascading back to URLSearchParams.
783+ // Do this to avoid any internal confusion about whether URLSearchParams or URL is up-to-date.
784+ if ( obj . #context. hasSearch ) {
785+ obj . #updateContext( bindingUrl . update ( obj . #context. href , updateActions . kSearch , '' ) ) ;
786+ }
787+ } ;
788+ }
772789
773790 constructor ( input , base = undefined ) {
774791 markTransferMode ( this , false , false ) ;
@@ -814,7 +831,37 @@ class URL {
814831 return `${ constructor . name } ${ inspect ( obj , opts ) } ` ;
815832 }
816833
817- #updateContext( href ) {
834+ #getSearchFromContext( ) {
835+ if ( ! this . #context. hasSearch ) return '' ;
836+ let endsAt = this . #context. href . length ;
837+ if ( this . #context. hasHash ) endsAt = this . #context. hash_start ;
838+ if ( endsAt - this . #context. search_start <= 1 ) return '' ;
839+ return StringPrototypeSlice ( this . #context. href , this . #context. search_start , endsAt ) ;
840+ }
841+
842+ #getSearchFromParams( ) {
843+ if ( ! this . #searchParams?. size ) return '' ;
844+ return `?${ this . #searchParams} ` ;
845+ }
846+
847+ #ensureSearchParamsUpdated( ) {
848+ // URL is updated lazily to greatly improve performance when URLSearchParams is updated repeatedly.
849+ // If URLSearchParams has been modified, reflect that back into URL, without cascading back.
850+ if ( this . #searchParamsModified) {
851+ this . #searchParamsModified = false ;
852+ this . #updateContext( bindingUrl . update ( this . #context. href , updateActions . kSearch , this . #getSearchFromParams( ) ) ) ;
853+ }
854+ }
855+
856+ /**
857+ * Update the internal context state for URL.
858+ * @param {string } href New href string from `bindingUrl.update`.
859+ * @param {boolean } [shouldUpdateSearchParams] If the update has potential to update search params (href/search).
860+ */
861+ #updateContext( href , shouldUpdateSearchParams = false ) {
862+ const previousSearch = shouldUpdateSearchParams && this . #searchParams &&
863+ ( this . #searchParamsModified ? this . #getSearchFromParams( ) : this . #getSearchFromContext( ) ) ;
864+
818865 this . #context. href = href ;
819866
820867 const {
@@ -840,27 +887,39 @@ class URL {
840887 this . #context. scheme_type = scheme_type ;
841888
842889 if ( this . #searchParams) {
843- if ( this . #context. hasSearch ) {
844- setURLSearchParams ( this . #searchParams, this . search ) ;
845- } else {
846- setURLSearchParams ( this . #searchParams, undefined ) ;
890+ // If the search string has updated, URL becomes the source of truth, and we update URLSearchParams.
891+ // Only do this when we're expecting it to have changed, otherwise a change to hash etc.
892+ // would incorrectly compare the URLSearchParams state to the empty URL search state.
893+ if ( shouldUpdateSearchParams ) {
894+ const currentSearch = this . #getSearchFromContext( ) ;
895+ if ( previousSearch !== currentSearch ) {
896+ setURLSearchParams ( this . #searchParams, currentSearch ) ;
897+ this . #searchParamsModified = false ;
898+ }
847899 }
900+
901+ // If we have a URLSearchParams, ensure that URL is up-to-date with any modification to it.
902+ this . #ensureSearchParamsUpdated( ) ;
848903 }
849904 }
850905
851906 toString ( ) {
907+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
908+ this . #ensureSearchParamsUpdated( ) ;
852909 return this . #context. href ;
853910 }
854911
855912 get href ( ) {
913+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
914+ this . #ensureSearchParamsUpdated( ) ;
856915 return this . #context. href ;
857916 }
858917
859918 set href ( value ) {
860919 value = `${ value } ` ;
861920 const href = bindingUrl . update ( this . #context. href , updateActions . kHref , value ) ;
862921 if ( ! href ) { throw new ERR_INVALID_URL ( value ) ; }
863- this . #updateContext( href ) ;
922+ this . #updateContext( href , true ) ;
864923 }
865924
866925 // readonly
@@ -1002,26 +1061,25 @@ class URL {
10021061 }
10031062
10041063 get search ( ) {
1005- if ( ! this . #context. hasSearch ) { return '' ; }
1006- let endsAt = this . #context. href . length ;
1007- if ( this . #context. hasHash ) { endsAt = this . #context. hash_start ; }
1008- if ( endsAt - this . #context. search_start <= 1 ) { return '' ; }
1009- return StringPrototypeSlice ( this . #context. href , this . #context. search_start , endsAt ) ;
1064+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
1065+ this . #ensureSearchParamsUpdated( ) ;
1066+ return this . #getSearchFromContext( ) ;
10101067 }
10111068
10121069 set search ( value ) {
10131070 const href = bindingUrl . update ( this . #context. href , updateActions . kSearch , StringPrototypeToWellFormed ( `${ value } ` ) ) ;
10141071 if ( href ) {
1015- this . #updateContext( href ) ;
1072+ this . #updateContext( href , true ) ;
10161073 }
10171074 }
10181075
10191076 // readonly
10201077 get searchParams ( ) {
10211078 // Create URLSearchParams on demand to greatly improve the URL performance.
10221079 if ( this . #searchParams == null ) {
1023- this . #searchParams = new URLSearchParams ( this . search ) ;
1080+ this . #searchParams = new URLSearchParams ( this . #getSearchFromContext ( ) ) ;
10241081 setURLSearchParamsContext ( this . #searchParams, this ) ;
1082+ this . #searchParamsModified = false ;
10251083 }
10261084 return this . #searchParams;
10271085 }
@@ -1041,6 +1099,8 @@ class URL {
10411099 }
10421100
10431101 toJSON ( ) {
1102+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
1103+ this . #ensureSearchParamsUpdated( ) ;
10441104 return this . #context. href ;
10451105 }
10461106
0 commit comments