@@ -202,6 +202,7 @@ class URLContext {
202202 }
203203}
204204
205+ let setURLSearchParamsModified ;
205206let setURLSearchParamsContext ;
206207let getURLSearchParamsList ;
207208let setURLSearchParams ;
@@ -469,8 +470,9 @@ class URLSearchParams {
469470 name = StringPrototypeToWellFormed ( `${ name } ` ) ;
470471 value = StringPrototypeToWellFormed ( `${ value } ` ) ;
471472 ArrayPrototypePush ( this . #searchParams, name , value ) ;
473+
472474 if ( this . #context) {
473- this . #context. search = this . toString ( ) ;
475+ setURLSearchParamsModified ( this . #context) ;
474476 }
475477 }
476478
@@ -503,8 +505,9 @@ class URLSearchParams {
503505 }
504506 }
505507 }
508+
506509 if ( this . #context) {
507- this . #context. search = this . toString ( ) ;
510+ setURLSearchParamsModified ( this . #context) ;
508511 }
509512 }
510513
@@ -609,7 +612,7 @@ class URLSearchParams {
609612 }
610613
611614 if ( this . #context) {
612- this . #context. search = this . toString ( ) ;
615+ setURLSearchParamsModified ( this . #context) ;
613616 }
614617 }
615618
@@ -658,7 +661,7 @@ class URLSearchParams {
658661 }
659662
660663 if ( this . #context) {
661- this . #context. search = this . toString ( ) ;
664+ setURLSearchParamsModified ( this . #context) ;
662665 }
663666 }
664667
@@ -763,6 +766,20 @@ function isURL(self) {
763766class URL {
764767 #context = new URLContext ( ) ;
765768 #searchParams;
769+ #searchParamsModified;
770+
771+ static {
772+ setURLSearchParamsModified = ( obj ) => {
773+ // When URLSearchParams changes, we lazily update URL on the next read/write for performance.
774+ obj . #searchParamsModified = true ;
775+
776+ // If URL has an existing search, remove it without cascading back to URLSearchParams.
777+ // Do this to avoid any internal confusion about whether URLSearchParams or URL is up-to-date.
778+ if ( obj . #context. hasSearch ) {
779+ obj . #updateContext( bindingUrl . update ( obj . #context. href , updateActions . kSearch , '' ) ) ;
780+ }
781+ } ;
782+ }
766783
767784 constructor ( input , base = undefined ) {
768785 if ( arguments . length === 0 ) {
@@ -806,7 +823,37 @@ class URL {
806823 return `${ constructor . name } ${ inspect ( obj , opts ) } ` ;
807824 }
808825
809- #updateContext( href ) {
826+ #getSearchFromContext( ) {
827+ if ( ! this . #context. hasSearch ) return '' ;
828+ let endsAt = this . #context. href . length ;
829+ if ( this . #context. hasHash ) endsAt = this . #context. hash_start ;
830+ if ( endsAt - this . #context. search_start <= 1 ) return '' ;
831+ return StringPrototypeSlice ( this . #context. href , this . #context. search_start , endsAt ) ;
832+ }
833+
834+ #getSearchFromParams( ) {
835+ if ( ! this . #searchParams?. size ) return '' ;
836+ return `?${ this . #searchParams} ` ;
837+ }
838+
839+ #ensureSearchParamsUpdated( ) {
840+ // URL is updated lazily to greatly improve performance when URLSearchParams is updated repeatedly.
841+ // If URLSearchParams has been modified, reflect that back into URL, without cascading back.
842+ if ( this . #searchParamsModified) {
843+ this . #searchParamsModified = false ;
844+ this . #updateContext( bindingUrl . update ( this . #context. href , updateActions . kSearch , this . #getSearchFromParams( ) ) ) ;
845+ }
846+ }
847+
848+ /**
849+ * Update the internal context state for URL.
850+ * @param {string } href New href string from `bindingUrl.update`.
851+ * @param {boolean } [shouldUpdateSearchParams] If the update has potential to update search params (href/search).
852+ */
853+ #updateContext( href , shouldUpdateSearchParams = false ) {
854+ const previousSearch = shouldUpdateSearchParams && this . #searchParams &&
855+ ( this . #searchParamsModified ? this . #getSearchFromParams( ) : this . #getSearchFromContext( ) ) ;
856+
810857 this . #context. href = href ;
811858
812859 const {
@@ -832,27 +879,39 @@ class URL {
832879 this . #context. scheme_type = scheme_type ;
833880
834881 if ( this . #searchParams) {
835- if ( this . #context. hasSearch ) {
836- setURLSearchParams ( this . #searchParams, this . search ) ;
837- } else {
838- setURLSearchParams ( this . #searchParams, undefined ) ;
882+ // If the search string has updated, URL becomes the source of truth, and we update URLSearchParams.
883+ // Only do this when we're expecting it to have changed, otherwise a change to hash etc.
884+ // would incorrectly compare the URLSearchParams state to the empty URL search state.
885+ if ( shouldUpdateSearchParams ) {
886+ const currentSearch = this . #getSearchFromContext( ) ;
887+ if ( previousSearch !== currentSearch ) {
888+ setURLSearchParams ( this . #searchParams, currentSearch ) ;
889+ this . #searchParamsModified = false ;
890+ }
839891 }
892+
893+ // If we have a URLSearchParams, ensure that URL is up-to-date with any modification to it.
894+ this . #ensureSearchParamsUpdated( ) ;
840895 }
841896 }
842897
843898 toString ( ) {
899+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
900+ this . #ensureSearchParamsUpdated( ) ;
844901 return this . #context. href ;
845902 }
846903
847904 get href ( ) {
905+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
906+ this . #ensureSearchParamsUpdated( ) ;
848907 return this . #context. href ;
849908 }
850909
851910 set href ( value ) {
852911 value = `${ value } ` ;
853912 const href = bindingUrl . update ( this . #context. href , updateActions . kHref , value ) ;
854913 if ( ! href ) { throw new ERR_INVALID_URL ( value ) ; }
855- this . #updateContext( href ) ;
914+ this . #updateContext( href , true ) ;
856915 }
857916
858917 // readonly
@@ -994,26 +1053,25 @@ class URL {
9941053 }
9951054
9961055 get search ( ) {
997- if ( ! this . #context. hasSearch ) { return '' ; }
998- let endsAt = this . #context. href . length ;
999- if ( this . #context. hasHash ) { endsAt = this . #context. hash_start ; }
1000- if ( endsAt - this . #context. search_start <= 1 ) { return '' ; }
1001- return StringPrototypeSlice ( this . #context. href , this . #context. search_start , endsAt ) ;
1056+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
1057+ this . #ensureSearchParamsUpdated( ) ;
1058+ return this . #getSearchFromContext( ) ;
10021059 }
10031060
10041061 set search ( value ) {
10051062 const href = bindingUrl . update ( this . #context. href , updateActions . kSearch , StringPrototypeToWellFormed ( `${ value } ` ) ) ;
10061063 if ( href ) {
1007- this . #updateContext( href ) ;
1064+ this . #updateContext( href , true ) ;
10081065 }
10091066 }
10101067
10111068 // readonly
10121069 get searchParams ( ) {
10131070 // Create URLSearchParams on demand to greatly improve the URL performance.
10141071 if ( this . #searchParams == null ) {
1015- this . #searchParams = new URLSearchParams ( this . search ) ;
1072+ this . #searchParams = new URLSearchParams ( this . #getSearchFromContext ( ) ) ;
10161073 setURLSearchParamsContext ( this . #searchParams, this ) ;
1074+ this . #searchParamsModified = false ;
10171075 }
10181076 return this . #searchParams;
10191077 }
@@ -1033,6 +1091,8 @@ class URL {
10331091 }
10341092
10351093 toJSON ( ) {
1094+ // Updates to URLSearchParams are lazily propagated to URL, so we need to check we're in sync.
1095+ this . #ensureSearchParamsUpdated( ) ;
10361096 return this . #context. href ;
10371097 }
10381098
0 commit comments