| | 558 | |
| | 559 | /** |
| | 560 | * Optimizes script loading order to reduce parser blocking time. |
| | 561 | * |
| | 562 | * Reorders scripts so that async and defer scripts are processed first, |
| | 563 | * allowing them to download in parallel while blocking scripts execute, |
| | 564 | * thereby reducing DOMContentLoaded timing. |
| | 565 | * |
| | 566 | * @since 6.8.0 |
| | 567 | */ |
| | 568 | private function optimize_loading_order() { |
| | 569 | if ( ! $this->optimize_loading_order_enabled || empty( $this->to_do ) ) { |
| | 570 | return; |
| | 571 | } |
| | 572 | |
| | 573 | $start_time = microtime( true ); |
| | 574 | $original_order = $this->to_do; |
| | 575 | |
| | 576 | // Group scripts by loading priority |
| | 577 | $script_priorities = array(); |
| | 578 | $dependency_map = array(); |
| | 579 | |
| | 580 | // Build dependency map and calculate priorities |
| | 581 | foreach ( $this->to_do as $handle ) { |
| | 582 | if ( isset( $this->registered[ $handle ] ) ) { |
| | 583 | $script = $this->registered[ $handle ]; |
| | 584 | $strategy = $this->get_eligible_loading_strategy( $handle, 'blocking' ); |
| | 585 | $priority = $this->calculate_loading_priority( $handle, $strategy ); |
| | 586 | |
| | 587 | $script_priorities[ $handle ] = $priority; |
| | 588 | $dependency_map[ $handle ] = $script->deps ?? array(); |
| | 589 | } |
| | 590 | } |
| | 591 | |
| | 592 | // Sort scripts by priority while maintaining dependency order |
| | 593 | $optimized_order = $this->sort_with_dependencies( $script_priorities, $dependency_map ); |
| | 594 | |
| | 595 | // Update the to_do array with optimized order |
| | 596 | $this->to_do = array_values( $optimized_order ); |
| | 597 | |
| | 598 | // Performance monitoring hook |
| | 599 | if ( function_exists( 'do_action' ) ) { |
| | 600 | $end_time = microtime( true ); |
| | 601 | do_action( 'wp_script_optimization_complete', array( |
| | 602 | 'execution_time' => $end_time - $start_time, |
| | 603 | 'scripts_processed' => count( $original_order ), |
| | 604 | 'original_order' => $original_order, |
| | 605 | 'optimized_order' => $this->to_do, |
| | 606 | ) ); |
| | 607 | } |
| | 608 | } |
| | 609 | |
| | 610 | /** |
| | 611 | * Calculates loading priority for a script based on its strategy and characteristics. |
| | 612 | * |
| | 613 | * Lower numbers = higher priority (loaded first) |
| | 614 | * Priority order: async (1) -> defer (2) -> no-deps blocking (3) -> deps blocking (4) |
| | 615 | * |
| | 616 | * @since 6.8.0 |
| | 617 | * |
| | 618 | * @param string $handle Script handle. |
| | 619 | * @param string $strategy Loading strategy ('async', 'defer', or 'blocking'). |
| | 620 | * @return int Loading priority. |
| | 621 | */ |
| | 622 | private function calculate_loading_priority( $handle, $strategy ) { |
| | 623 | switch ( $strategy ) { |
| | 624 | case 'async': |
| | 625 | return 1; // Highest priority - non-blocking, can load immediately |
| | 626 | |
| | 627 | case 'defer': |
| | 628 | return 2; // Second priority - non-blocking but ordered |
| | 629 | |
| | 630 | case 'blocking': |
| | 631 | default: |
| | 632 | // Blocking scripts get lower priority, but consider dependencies |
| | 633 | $script = $this->registered[ $handle ]; |
| | 634 | $has_deps = ! empty( $script->deps ); |
| | 635 | $has_inline = ! empty( $script->extra['after'] ) || ! empty( $script->extra['before'] ); |
| | 636 | |
| | 637 | if ( $has_deps || $has_inline ) { |
| | 638 | return 4; // Lowest priority - blocking with dependencies/inline scripts |
| | 639 | } |
| | 640 | |
| | 641 | return 3; // Low priority - simple blocking scripts |
| | 642 | } |
| | 643 | } |
| | 644 | |
| | 645 | /** |
| | 646 | * Sorts scripts by priority while maintaining dependency order. |
| | 647 | * |
| | 648 | * Uses topological sorting to ensure dependencies are processed before |
| | 649 | * their dependents, while optimizing for loading performance. |
| | 650 | * |
| | 651 | * @since 6.8.0 |
| | 652 | * |
| | 653 | * @param array $priorities Script priorities keyed by handle. |
| | 654 | * @param array $dependencies Dependency map keyed by handle. |
| | 655 | * @return array Optimized script order. |
| | 656 | */ |
| | 657 | private function sort_with_dependencies( $priorities, $dependencies ) { |
| | 658 | $sorted = array(); |
| | 659 | $visited = array(); |
| | 660 | $visiting = array(); |
| | 661 | |
| | 662 | // Group scripts by priority |
| | 663 | $priority_groups = array(); |
| | 664 | foreach ( $priorities as $handle => $priority ) { |
| | 665 | $priority_groups[ $priority ][] = $handle; |
| | 666 | } |
| | 667 | |
| | 668 | // Sort each priority group while respecting dependencies |
| | 669 | ksort( $priority_groups ); |
| | 670 | foreach ( $priority_groups as $priority => $handles ) { |
| | 671 | foreach ( $handles as $handle ) { |
| | 672 | $this->topological_sort_visit( $handle, $dependencies, $visited, $visiting, $sorted ); |
| | 673 | } |
| | 674 | } |
| | 675 | |
| | 676 | return $sorted; |
| | 677 | } |
| | 678 | |
| | 679 | /** |
| | 680 | * Performs topological sort visit for dependency resolution. |
| | 681 | * |
| | 682 | * @since 6.8.0 |
| | 683 | * |
| | 684 | * @param string $handle Current script handle. |
| | 685 | * @param array $dependencies Dependency map. |
| | 686 | * @param array &$visited Visited handles. |
| | 687 | * @param array &$visiting Currently visiting handles (cycle detection). |
| | 688 | * @param array &$sorted Sorted result array. |
| | 689 | */ |
| | 690 | private function topological_sort_visit( $handle, $dependencies, &$visited, &$visiting, &$sorted ) { |
| | 691 | if ( isset( $visiting[ $handle ] ) ) { |
| | 692 | // Circular dependency detected - maintain original order |
| | 693 | return; |
| | 694 | } |
| | 695 | |
| | 696 | if ( isset( $visited[ $handle ] ) ) { |
| | 697 | return; |
| | 698 | } |
| | 699 | |
| | 700 | $visiting[ $handle ] = true; |
| | 701 | |
| | 702 | // Visit dependencies first |
| | 703 | if ( isset( $dependencies[ $handle ] ) ) { |
| | 704 | foreach ( $dependencies[ $handle ] as $dep ) { |
| | 705 | // Visit dependency if it exists in our registered scripts or dependencies |
| | 706 | if ( isset( $dependencies[ $dep ] ) || isset( $this->registered[ $dep ] ) ) { |
| | 707 | $this->topological_sort_visit( $dep, $dependencies, $visited, $visiting, $sorted ); |
| | 708 | } |
| | 709 | } |
| | 710 | } |
| | 711 | |
| | 712 | unset( $visiting[ $handle ] ); |
| | 713 | $visited[ $handle ] = true; |
| | 714 | $sorted[] = $handle; |
| | 715 | } |
| | 716 | |
| | 717 | /** |
| | 718 | * Disables script loading order optimization. |
| | 719 | * |
| | 720 | * @since 6.8.0 |
| | 721 | */ |
| | 722 | public function disable_loading_order_optimization() { |
| | 723 | $this->optimize_loading_order_enabled = false; |
| | 724 | } |
| | 725 | |
| | 726 | /** |
| | 727 | * Enables script loading order optimization. |
| | 728 | * |
| | 729 | * @since 6.8.0 |
| | 730 | */ |
| | 731 | public function enable_loading_order_optimization() { |
| | 732 | $this->optimize_loading_order_enabled = true; |
| | 733 | } |